Sentinel Oraclev1.0

Merge Authority
Isolation.

Sentinel Oracle is a physically isolated merge authorization server that separates development capability from merge authority across three independent devices. No cloud dependency. No public ports. Cryptographic challenge-response protocol with biometric WebAuthn verification.

01

Abstract

Sentinel Oracle addresses a fundamental weakness in software supply chain security: the conflation of development capability with deployment authority. In conventional CI/CD pipelines, the same workstation used to write, build, and test code also holds the credentials (SSH keys, API tokens, GitHub personal access tokens) to merge pull requests and deploy to production. This creates a single point of compromise: any vulnerability that grants remote code execution on a developer workstation—a malicious npm package, a compromised VS Code extension, a phishing attack—gives the attacker full merge and deploy authority.

Sentinel Oracle separates these privileges across three physically independent devices connected via a zero-trust mesh network (Tailscale/WireGuard). The workstation displays merge authorization requests. The oracle server executes merge operations using credentials it holds exclusively. The phone provides biometric identity verification via WebAuthn. No single device, if compromised, can authorize a merge.

The protocol uses HMAC-SHA256 challenge-response with cryptographic binding to specific pull request identifiers, one-time challenge consumption, a 45-second TTL, and a global emergency lockdown mechanism. The system has no cloud dependency, exposes no public ports, and operates entirely within the operator's local network or tailnet.

02

Architecture — Three-Device Trust Model

Workstation
untrusted
Browser dashboard
Polls oracle via Tailscale
Displays QR challenges
No merge credentials
Phone
identity proof
WebAuthn passkey
Biometric authentication
Scans QR via camera
No GitHub access
Oracle Server
trusted authority
HMAC challenge generation
WebAuthn assertion verification
GitHub merge API execution
Holds merge credentials (PAT or GitHub App installation token)
ALL TRAFFIC VIA TAILSCALE MESH — WireGuard encrypted, zero-trust, no public ports

Workstation (Device 1, Untrusted).The developer's daily machine. Runs the IDE, browser, node_modules, third-party extensions. Polls the oracle dashboard via HTTPS over Tailscale. Displays merge authorization requests (QR codes) and their status. The workstation never holds GitHub credentials with merge scope. The oracle dashboard is read-only: no API endpoint on the oracle server accepts merge commands from the workstation.

Oracle Server (Device 2, Trusted). A dedicated physical device (Raspberry Pi, Intel NUC, thin client, mini PC, or an old Android phone running Termux) running the sentinel-oracle server. Connects to GitHub via a fine-grained personal access token or a GitHub App installation token, both scoped to merge pull requests. Polls GitHub for open PRs that have passed CI. Generates HMAC-signed challenges. Verifies WebAuthn assertions. Executes merge operations. Exposes an HTTPS dashboard bound to the Tailscale interface only. No ports are open to the public internet.

Phone (Device 3, Identity Proof).The operator's personal smartphone. Registers a WebAuthn passkey (platform authenticator, biometric-bound) with the oracle server. When a merge requires authorization, the phone scans a QR code displayed on the workstation dashboard, performs biometric verification, and sends a cryptographically signed assertion back to the oracle server. The assertion includes the challenge, the PR number, and a timestamp, all signed by the passkey's private key.

Rationale for physical isolation. A virtual machine or container on the same host does not provide sufficient separation. If the host kernel is compromised, all guest operating systems are compromised. A separate physical device with its own network interface, power supply, and boot chain ensures that a workstation compromise cannot reach the merge authority. The oracle server should be a dedicated device running no other workloads.
03

Cryptographic Protocol Specification

The protocol consists of four phases: challenge generation, QR encoding, WebAuthn assertion, and verifiable merge execution. All cryptographic operations use the Web Crypto API (subtle) on both the server and client sides.

3.1 Challenge Generation

HMAC_KEY = HKDF-SHA256(masterSecret, salt="sentinel-oracle-v1", info=serverFingerprint)

function GenerateChallenge(prNumber: number, sessionId: string): Challenge {
  nonce = crypto.getRandomValues(new Uint8Array(32))   // 256-bit random
  payload = JSON.stringify({
    pr:     prNumber,
    nonce:  hex(nonce),
    sessionId,
    ttl:    Date.now() + 45_000,      // 45 second validity window
  })
  signature = HMAC-SHA256(HMAC_KEY, payload)
  return { payload, signature }
}
  • The HMAC key is derived once at server startup via HKDF from the configured master secret.
  • Each challenge is bound to exactly one PR number. A challenge generated for PR #42 cannot authorize PR #17.
  • The nonce ensures uniqueness. If two challenges are generated for the same PR, they have different nonces.
  • The TTL (45 seconds) limits the window for replay. The server rejects challenges with expired TTL.

3.2 QR Encoding

challengeId = SHA256(payload + signature).slice(0, 16)   // 8-byte challenge ID
qrPayload = JSON.stringify({
  v:     1,                              // protocol version
  cid:   challengeId,                     // challenge identifier
  sig:   hex(signature),                  // HMAC signature
  host:  "https://100.x.y.z:3443",        // oracle server Tailscale IP
})

// QR is displayed on the workstation dashboard as an HTML canvas QR code.
// The phone scans it via the device camera using a WebRTC-based QR scanner.
  • The QR is displayed once and never re-displayed for the same challenge.
  • The host field tells the phone which Tailscale IP to send the assertion to.
  • The challenge ID is used by the server to look up challenge state on assertion receipt.

3.3 WebAuthn Assertion

// On the phone, after scanning the QR:
assertion = await navigator.credentials.get({
  publicKey: {
    challenge:      new Uint8Array(challengeId),           // bound to challenge
    allowCredentials: [{ id: credentialId, type: "public-key" }],
    userVerification: "required",                          // forces biometric
    timeout:          40_000,                              // 40 second window
  }
})

// The assertion is sent to the oracle server:
POST https://100.x.y.z:3443/api/authorize
Content-Type: application/json

{
  "challengeId": "a1b2c3d4e5f6a7b8",
  "credentialId": hex(assertion.id),
  "authenticatorData": base64(assertion.response.authenticatorData),
  "clientDataJSON": base64(assertion.response.clientDataJSON),
  "signature": base64(assertion.response.signature),
  "prNumber": 42
}
Why challenge is the WebAuthn challenge field. The WebAuthn specification defines the challengefield as a cryptographic random value that the authenticator signs along with other data. By using the challenge ID (derived from the HMAC-signed challenge payload) as the WebAuthn challenge, we create a cryptographic chain: the phone's assertion cannot be forged without both the HMAC key (held only by the oracle server) and the WebAuthn private key (held only by the phone's secure enclave).

3.4 Verifiable Merge Execution

function HandleAuthorize(req): MergeResult {
  // 1. Look up challenge by challengeId
  challenge = db.challenges.get(req.challengeId)
  if (!challenge)                return { error: "challenge not found" }
  if (challenge.consumed)         return { error: "challenge already used" }
  if (Date.now() > challenge.ttl) return { error: "challenge expired" }

  // 2. Verify HMAC integrity
  expected = HMAC-SHA256(HMAC_KEY, challenge.payload)
  if (!constantTimeEqual(expected, challenge.signature))
    return { error: "challenge integrity check failed" }

  // 3. Verify WebAuthn assertion
  assertionOk = VerifyWebAuthnAssertion(req.assertion, challengeId)
  if (!assertionOk) return { error: "WebAuthn assertion invalid" }

  // 4. Verify PR binding
  payload = JSON.parse(challenge.payload)
  if (payload.pr !== req.prNumber)
    return { error: "PR number mismatch" }

  // 5. Mark challenge consumed (atomic)
  db.challenges.markConsumed(challengeId)

  // 6. Execute GitHub merge
  result = github.mergePullRequest({
    owner:  config.ghOwner,
    repo:   config.ghRepo,
    prNumber: req.prNumber,
    mergeMethod: "squash",
  })

  return { success: true, mergeResult: result }
}
  • Steps 1-4 are designed such that any single failure aborts the operation with no state mutation.
  • Challenge consumption (step 5) is the first state mutation. It happens before the GitHub API call to prevent race conditions on retry.
  • If the GitHub API call fails, the challenge remains consumed (a consumed challenge is never re-usable). The operator must generate a new challenge.
  • The HMAC comparison uses constant-time comparison to prevent timing side-channel attacks.
04

Authorization Flow — Step by Step

17 steps across 4 phases involving all 3 devices. Each step is color-coded by device: oracle server,phone,workstation.

Oracle Phone Workstation
1WorkstationOpen dashboard on workstation browser
2OraclePoll GitHub for open PRs that passed CI
3OracleGenerate HMAC-SHA256 challenge (45s TTL, bound to PR)
4OracleStore challenge in SQLite database
5OracleReturn QR payload to workstation dashboard
6WorkstationDisplay QR code on workstation screen
7PhoneScan QR with phone camera
8PhoneParse challenge payload from QR
9PhoneWebAuthn biometric prompt on phone
10PhoneSign assertion with passkey private key
11PhonePOST signed assertion to /api/authorize
12OracleVerify HMAC signature integrity on challenge
13OracleVerify WebAuthn assertion validity
14OracleVerify PR number matches challenge binding
15OracleMark challenge as consumed (atomic)
16OracleCall GitHub merge API with PAT
17OracleReturn merge result to workstation dashboard
Round trip: ~10-20s|Critical window: 40s|Challenge TTL: 45s

The flow guarantees that all three devices participate in every merge authorization. If any device is unavailable, the flow cannot complete. If the workstation is compromised, the attacker can display arbitrary QR codes but cannot authenticate them (they lack the phone's biometric). If the phone is compromised, the attacker has identity but no HMAC challenge (the oracle generates challenges on demand and only displays them on the workstation). If the oracle server is compromised, all merges are at risk—this is the single trusted component and must be physically secured.

05

Network Topology

Tailscale Tailnet — 100.x.y.z / WireGuard Mesh
Workstation
100.1.2.3
inbound: OFF
service: dashboard (browser)
Phone
100.1.2.4
inbound: OFF
service: webauthn (browser)
Oracle
100.1.2.5
inbound: 3443 (HTTPS)
service: oracle API (node)
outbound HTTPS only
Public Internet
GitHub API
api.github.com:443
oracle only (outbound)
NTP
pool.ntp.org:123
all devices (clock sync)

Key properties. The oracle server listens on port 3443 (HTTPS) bound to the Tailscale interface only. No ports are exposed on the physical ethernet/Wi-Fi interface. The workstation and phone connect exclusively via Tailscale IP addresses. The only outbound connection from the oracle server to the public internet is to api.github.com:443 for merge operations and to NTP servers for clock synchronization.

TLS configuration.The oracle server uses a self-signed certificate generated at setup time. Tailscale provides transport encryption (WireGuard) between all nodes. The self-signed certificate protects against non-Tailscale attackers on the local network. Within a tailnet, Tailscale's WireGuard encryption is sufficient; the self-signed TLS is a defense-in-depth measure. For production deployments, replace the self-signed certificate with a Tailscale-issued certificate (via tailscale cert) or a Let's Encrypt certificate.

5.1 Traffic Flow Matrix

workstation → oracleHTTPS GET /api/dashboard, /api/status
phone → oracleHTTPS POST /api/authorize (WebAuthn assertion)
oracle → githubHTTPS POST /repos/:owner/:repo/pulls/:number/merge
oracle → phoneHTTPS response to /api/authorize (result)
oracle → workstationHTTPS response to /api/dashboard (QR + status)
phone → workstationNONE (QR is optical, out-of-band)
workstation → githubNONE (workstation has no merge token)
06

Threat Model & Attack Vector Analysis

Each attack vector is analyzed for assets at risk, controls, and residual risk. Assumes the oracle server is physically secured and runs no other workloads.

Workstation RCE
Assets at risk: oracle dashboard session (read-only). Controls: no merge credentials stored on workstation; dashboard is read-only; no API endpoint accepts merge commands from workstation. Residual risk: attacker can display fake QR codes, but cannot complete the authorization flow without phone biometric.
Phone theft / loss
Assets at risk: WebAuthn passkey private key. Controls: passkey is biometric-bound (Face ID / fingerprint); device PIN required after restart; remote wipe via MDM. Residual risk: advanced attacker with device unlock and live biometric could authorize merges within the 45s challenge window.
Oracle server physical theft
Assets at risk: GitHub PAT with merge scope, HMAC master secret, WebAuthn credential IDs. Controls: full-disk encryption (LUKS/BitLocker); BIOS password; secure boot; tamper-evident enclosure. Residual risk: attacker with unlimited physical access, FDE passphrase, and no lockdown trigger can extract all secrets.
Oracle server remote compromise
Assets at risk: all merge authority. Controls: oracle runs no other services; minimal OS install; Tailscale ACLs restrict access to oracle server port 3443; HTTPS-only API; no writable endpoints without authentication. Residual risk: zero-day in Node.js or Tailscale daemon.
Network MITM
Assets at risk: challenge payload, WebAuthn assertion in transit. Controls: Tailscale WireGuard encrypts all traffic; self-signed TLS provides defense-in-depth; challenges are single-use with 45s TTL. Residual risk: attacker on the tailnet node itself (see Tailscale ACLs).
Replay attack
Assets at risk: re-use of captured challenge. Controls: challenges are consumed on first use (atomic DB update); TTL of 45 seconds; challenge is bound to specific PR number. Residual risk: zero (challenge is single-use by design).
QR phishing
Assets at risk: phone sends WebAuthn assertion to attacker server. Controls: user must visually verify the PR number on the workstation dashboard matches the PR they intend to merge; QR contains the oracle server host field. Residual risk: user error if the operator does not verify the PR number or host.
Timing side-channel
Assets at risk: HMAC key recovery. Controls: constant-time comparison (crypto.timingSafeEqual in Node.js). Residual risk: negligible with constant-time implementation.
Denial of service
Assets at risk: legitimate merges blocked. Controls: rate limiting on challenge generation (max 5/min/session); challenge GC (expired challenges purged). Residual risk: temporary disruption, no permanent data loss.
Emergency lockdown bypass
Assets at risk: lockdown does not take effect. Controls: lockdown status stored on disk, checked on every API request, persists across restarts. Residual risk: file system corruption (redundant: lockdown re-applied on startup via script).
Emergency lockdown procedure. When lockdown is activated (via the dashboard or a physical button on the oracle server), the server immediately: (1) invalidates all pending challenges in the database, (2) sets all open PRs to a failure/blocked state, (3) rejects all new challenge generation requests, and (4) persists the lockdown flag to disk. Lockdown is deactivated manually by the operator with physical access to the oracle server. This is the nuclear option: it stops all merges until the operator can investigate the security incident.
07

Configuration Reference

All configuration is stored in ~/.sentinel-oracle/config.json. The file is created automatically on first run with default values.

githubOwnerGitHub repository owner (string)
githubRepoGitHub repository name (string)
githubTokenGitHub PAT with pull-requests:write scope. Optional if using GitHub App.
githubAppIdGitHub App ID for JWT authentication. Optional if using PAT.
githubInstallationIdGitHub App installation ID. Required with githubAppId.
githubPrivateKeyPathPath to GitHub App private key PEM file. Required with githubAppId.
githubStatusContextCommit status context name (default: "Sentinel Authorization")
portHTTPS listen port (default: 3443)
hostBind address (default: "0.0.0.0")
challengeTtlMsChallenge time-to-live in milliseconds (default: 45000)
rateLimitAuthMax authentication attempts per window (default: 5)
rateLimitWindowMsRate limit window in milliseconds (default: 60000)
approveReasonRequiredRequire reason for approval (boolean, default: false)
enrollmentTokenTtlMsEnrollment token refresh interval (default: 120000)
githubWebhookSecretSecret for verifying GitHub webhook payloads (optional)
encryptionKeyAES-256-GCM key for database encryption (auto-generated)

7.1 Authentication Modes

Sentinel Oracle supports two authentication modes for GitHub API access:

PAT mode (classic). Uses a fine-grained PAT with pull-requests:write scope. Simple to set up. Token is long-lived and must be rotated manually every 90 days.

GitHub App mode (recommended). Creates a GitHub App that generates installation tokens. Tokens expire every 60 minutes and auto-refresh. Repo-scoped, no user account dependency. See the full setup guide at the sentinel-oracle repository for step-by-step GitHub App creation instructions.

If both are configured, GitHub App mode takes precedence.

08

Quick Start

Choose any always-on device with a network connection and Node.js support: Raspberry Pi (2W+), Intel NUC, thin client, old Android phone via Termux, or a low-power VPS. The device must join the same Tailscale tailnet as your workstation and phone.

8.1 Clone and Install

git clone https://github.com/javier20dev25/sentinel-oracle.git
cd sentinel-oracle
npm install
npm run build

8.2 Configure

Two authentication modes are supported. GitHub App mode is recommended. See GITHUB_APP_SETUP.md for setup instructions.

# Create config file manually or let the server generate defaults on first run:
mkdir -p ~/.sentinel-oracle
cat > ~/.sentinel-oracle/config.json << 'EOF'

# Option A: PAT mode
{
  "githubToken": "github_pat_...",
  "githubOwner": "your-org",
  "githubRepo": "your-repo"
}

# Option B: GitHub App mode (recommended)
{
  "githubAppId": "123456",
  "githubInstallationId": "654321",
  "githubPrivateKeyPath": "/home/sentinel/.sentinel-oracle/key.pem",
  "githubOwner": "your-org",
  "githubRepo": "your-repo"
}
EOF

# Set secrets via environment variables (recommended):
export ORACLE_MASTER_SECRET="$(openssl rand -hex 32)"

8.3 Start

npm start

# Server listens on https://100.x.y.z:3443
# Open in browser on your phone via Tailscale to register a passkey.

8.4 Branch Protection Verification

Sentinel Oracle includes a branch protection verification endpoint (GET /api/status/branch-protection) that checks whether the main branch has the Sentinel Authorization status check configured as a required check. The polling cycle also verifies branch protection every 30 seconds and logs warnings if issues are detected. The dashboard displays the current branch protection status with any issues listed.

Required branch protection settings:

  • Require status checks to pass before merging
  • Sentinel Authorization must be a required status check
  • Do not allow bypassing the above settings (admins must pass too)
  • Require pull request reviews before merging (at least 1)
  • Dismiss stale pull request approvals when new commits are pushed

8.5 Webhook Receiver

Sentinel Oracle exposes a webhook receiver at POST /api/webhook/github that accepts GitHub webhook events. When configured, the receiver processes pull_request events (opened, synchronize, closed/merged) and push events to main. If an unauthorized merge is detected (a PR merged without passing through Oracle authorization), the event is logged to the audit log as an alert.

Configure the webhook in your GitHub repository Settings > Webhooks with the Payload URL pointing to your oracle server. Set the content type to application/json. The webhook secret is optional but recommended and can be configured via the githubWebhookSecret field in config.json.

8.6 PR Check Details and Metrics

Sentinel Oracle provides detailed check run information for each pull request. The GET /api/prs/{number}/checks endpoint returns the full list of check runs for the PR's commit, including check name, conclusion (success/failure/pending), duration, and diff statistics (files changed, lines added, lines removed). The dashboard displays this information in an expandable section on each PR card.

The GET /api/metrics endpoint returns aggregated data for audit and analysis: summary counts (total PRs, pending, authorized, rejected, expired), per-PR merge times with approval wait duration, and per-author statistics (merged count, rejected count, average wait time). This data can be used for trend analysis and auditing.

09

Deployment Guide

The oracle server runs on any device with Node.js 18+ and Tailscale. Common choices include a Raspberry Pi 2W+ (always-on, 5W power), an old Android phone running Termux, a repurposed thin client, or any Linux server. The device must have Tailscale installed and connected to the same tailnet as the workstation and phone.

9.1 Linux (systemd)

# /etc/systemd/system/sentinel-oracle.service
[Unit]
Description=Sentinel Oracle - Merge Authority Isolation Server
After=network-online.target tailscaled.service
Requires=tailscaled.service

[Service]
Type=simple
User=sentinel
Group=sentinel
WorkingDirectory=/opt/sentinel-oracle
Environment=NODE_ENV=production
Environment=GITHUB_TOKEN=ghp_xxxx...
Environment=ORACLE_MASTER_SECRET=$(cat /opt/sentinel-oracle/secret.key)
ExecStart=/usr/bin/node /opt/sentinel-oracle/dist/server.js
Restart=always
RestartSec=10
TimeoutStopSec=30

[Install]
WantedBy=multi-user.target
Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable --now sentinel-oracle
sudo systemctl status sentinel-oracle

9.2 Windows (NSSM)

nssm install SentinelOracle "C:\Program Files\nodejs\node.exe"
nssm set SentinelOracle AppParameters "C:\sentinel-oracle\dist\server.js"
nssm set SentinelOracle AppDirectory "C:\sentinel-oracle"
nssm set SentinelOracle AppEnvironmentExtra NODE_ENV=production GITHUB_TOKEN=... ORACLE_MASTER_SECRET=...
nssm set SentinelOracle Start SERVICE_AUTO_START
nssm start SentinelOracle

9.3 macOS (LaunchAgent)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.sentinel.oracle</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/local/bin/node</string>
    <string>/opt/sentinel-oracle/dist/server.js</string>
  </array>
  <key>WorkingDirectory</key>
  <string>/opt/sentinel-oracle</string>
  <key>EnvironmentVariables</key>
  <dict>
    <key>NODE_ENV</key>
    <string>production</string>
    <key>GITHUB_TOKEN</key>
    <string>ghp_xxxx...</string>
    <key>ORACLE_MASTER_SECRET</key>
    <string>xxxx</string>
  </dict>
  <key>KeepAlive</key>
  <true/>
  <key>RunAtLoad</key>
  <true/>
</dict>
</plist>

9.4 Android / Termux

# Install Termux from F-Droid (not Play Store)
pkg update && pkg upgrade
pkg install nodejs git tailscale

git clone https://github.com/javier20dev25/sentinel-oracle.git
cd sentinel-oracle
npm install && npm run build

# Keep the phone plugged in and the screen on
# Use termux-wake-lock to prevent CPU sleep

9.5 Tailscale Setup

# On each device:
sudo tailscale up --authkey tskey-xxxx

# Verify all devices are visible:
tailscale status
#   100.1.2.3   workstation    linux   active; direct
#   100.1.2.4   phone-iphone   iOS     active; direct
#   100.1.2.5   oracle         linux   active; direct

# Test connectivity:
curl -k https://100.1.2.5:3443/api/status

9.6 Verification Checklist

  • All three devices visible in tailscale status
  • Oracle dashboard accessible from workstation: https://100.1.2.5:3443
  • Oracle dashboard accessible from phone browser (via Tailscale)
  • GitHub PAT works: curl -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/repos/owner/repo
  • WebAuthn passkey registration works (register from phone browser)
  • Challenge generation and QR display works on the workstation dashboard
  • Full end-to-end merge: generate challenge → scan QR → biometric → merge
  • Emergency lockdown activates and deactivates correctly
  • Lockdown persists across server restart
  • Server restart does not corrupt the database
  • Clock sync (NTP) is enabled on the oracle server
  • Branch protection verification shows "Secure" with no issues
  • GitHub App installation token acquisition works (if using GitHub App mode)
  • Branch protection changes are detected within one polling cycle
  • Unauthorized merge detection logs an alert (if webhook is configured)
  • PR check details expandable section shows check run information
  • Metrics endpoint returns data with correct PR counts and statistics
10

Security Considerations

Physical security of the oracle server is critical. The oracle server is the single trusted component in the architecture. It should be located in a locked room or a locked enclosure. Full-disk encryption (LUKS for Linux, BitLocker for Windows) must be enabled. The boot process must require a passphrase. The BIOS/UEFI must be password-protected and configured to boot only from the internal drive. Secure Boot should be enabled.

GitHub PAT management.The GitHub PAT should be a fine-grained token with "pull requests: write" scope only. Rotate the token every 90 days. Use expires_atwhen creating the token.

Master secret entropy. The ORACLE_MASTER_SECRETmust have at least 256 bits of entropy (32 bytes):

openssl rand -hex 32 > /opt/sentinel-oracle/secret.key
chmod 600 /opt/sentinel-oracle/secret.key

Clock synchronization. WebAuthn relies on accurate timestamps. The oracle server must run NTP. A clock skew of more than 30 seconds will cause WebAuthn assertion verification to fail.

Logging and auditing. All merge authorization events should be logged with: timestamp, PR number, challenge ID, credential ID, client IP, and result (success/failure/denied). Forward logs to a centralized system for alerting and forensic analysis.

Regular key rotation. Rotate the master secret every 6 months. Rotate the GitHub PAT every 90 days. Existing challenges become invalid on key rotation, which is acceptable because challenges have a 45-second TTL.

GitHub App mode advantages. When using GitHub App authentication, installation tokens expire every 60 minutes and auto-refresh. There is no long-lived PAT to leak or rotate. The token is repo-scoped and cannot access other repositories. If the private key is compromised, it can be revoked from the GitHub App settings without affecting other services.

Branch protection auto-verification. The server periodically checks branch protection settings and logs warnings if the Sentinel Authorization status check is missing, admin bypass is enabled, or force pushes are allowed. These checks are read-only and do not modify GitHub settings.

Webhook integrity. If a webhook secret is configured, incoming webhook payloads are verified against the secret before processing. This prevents spoofed webhook events. The webhook receiver does not accept merge commands or any state-changing operations.

Backup strategy. The only persistent state is the SQLite database and config file. Back up the database daily, encrypted, and stored separately from the oracle server. Test restoration quarterly.

11

Frequently Asked Questions

Why a separate physical device instead of a VM or container?

A VM or container shares the host kernel. If the host is compromised, all VMs and containers on that host are compromised. A separate physical device ensures workstation compromise cannot reach the merge authority. The cost of a Raspberry Pi or NUC is negligible compared to the cost of a supply chain security incident.

Why Tailscale instead of a VPN or direct LAN?

Tailscale provides zero-config WireGuard mesh networking with automatic NAT traversal, consistent IP namespace (100.x.y.z) independent of physical topology, ACL-based access control, and no public ports. Direct LAN would work but requires manual IP configuration and port forwarding for phone access.

Why 45 seconds for the challenge TTL?

Long enough for the operator to look at the workstation screen, pick up their phone, open the camera, scan the QR, and complete biometric auth. Short enough to make replay attacks impractical. The 5-second buffer beyond the WebAuthn timeout (40 seconds) accounts for network latency.

What happens if the oracle server loses power?

No data is lost (SQLite WAL mode is crash-safe). When power is restored, the server resumes polling GitHub and accepting new authorizations. Lockdown state persists across power loss. NTP corrects clock drift automatically within minutes.

Can multiple operators share one oracle server?

Yes. Each operator registers their own WebAuthn passkey. Credential IDs are mapped to operator identities. The dashboard shows which operator authorized each merge. Designed for teams of 2-5 developers on a shared tailnet.

How does this protect against supply chain attacks on the oracle server itself?

The oracle server runs only sentinel-oracle and a minimal OS. No browser, no package manager (after setup), no interactive login except SSH. Attack surface is limited to: Node.js runtime, Tailscale daemon, Linux kernel, and the sentinel-oracle application code (minimal dependencies, audited with npm audit and Socket.dev).

Can I run this on an old Android phone?

Yes. Install Termux from F-Droid, then Node.js and Tailscale via pkg. Keep the phone plugged in and use termux-wake-lock to prevent CPU sleep. An old phone is a perfectly viable oracle server: ARM CPU, built-in battery backup, no moving parts, and you likely already have one in a drawer.

What is the performance impact of the authorization flow?

The authorization flow adds 10-20 seconds to the merge process (including human interaction). The oracle server polls GitHub every 15 seconds (configurable). The merge itself takes 1-3 seconds. Total overhead is negligible compared to the security benefit.

How do I choose between PAT and GitHub App authentication?

GitHub App mode is recommended for production deployments. Installation tokens expire every 60 minutes and auto-refresh, eliminating long-lived credentials. PAT mode is simpler to set up and suitable for evaluation or personal use. Both modes are fully supported and can be switched between without data loss. If both are configured, GitHub App mode takes precedence.

Sentinel Oracle — Technical Specification v1.0BUSSL-1.1