Physically isolated merge authorization server for GitHub pull requests. Merge authority resides on a separate device -- Raspberry Pi, NUC, mini PC, or an old Android phone -- on the local network. The workstation that develops code never holds the credentials to merge it.
This is not a replacement for GitHub branch protection rules. Oracle operates as an additional enforcement layer. Repository administrators with direct push or bypass permissions remain a valid threat path independent of Oracle's controls.
Misión: Separar físicamente la autoridad de merge del entorno de desarrollo. Sentinel Oracle garantiza que ninguna estación de trabajo comprometida —por malware, extensiones maliciosas, npm supply chain attacks o phishing— pueda fusionar código a producción sin autorización biométrica desde un dispositivo independiente.
El merge no es una operación de CI. Es un acto de autoridad que debe requerir presencia física y consentimiento explícito.
Visión: Un ecosistema donde el ciclo de vida del código tenga tres roles irreducibles: el desarrollador escribe y prueba, la CI verifica calidad, y un dispositivo físico aislado —el Oracle— concede el merge. Ningún ataque que comprometa solo un eslabón puede completar un merge malicioso.
Qué NO es: No es un linter, ni un reemplazo de branch protection, ni un code review tool, ni un CI/CD pipeline. Sentinel Oracle es una capa de autorización que cierra el último vector de ataque antes de producción: la workstation comprometida con credenciales de merge.
Two authentication modes are supported: Personal Access Token (PAT) and GitHub App (recommended). See docs/github-app-setup.md for detailed GitHub App setup instructions.
Full architecture, API reference, and operational guide are in the docs/ directory:
| Document | Description |
|---|---|
| docs/architecture.md | System architecture, module dependency graph, data flow, database schema |
| docs/api.md | Complete API reference with request/response examples |
| docs/guide.md | Operational guide: installation, configuration, CLI reference, troubleshooting, AI setup |
| docs/github-app-setup.md | GitHub App registration, installation, and configuration |
| docs/security-dna.md | Security DNA aggregator: design, data flow, validation results |
| docs/security-audit.md | Security audit report and findings |
| docs/attack-vectors.md | Attack vector analysis and threat modeling |
| docs/workflow-intelligence.md | CI workflow intelligence engine, detection modules, telemetry |
| docs/sarif-integration.md | SARIF report generation and export |
| docs/ai-agent-integration.md | AI agent skills for coding assistants |
| docs/crypto-audit.md | Cryptographic audit: key rotation, integrity, keychain |
| docs/inventory-drift.md | Token inventory drift detection |
| docs/tailscale-integration.md | Tailscale client configuration and version detection |
git clone https://github.com/javier20dev25/sentinel-oracle.git
cd sentinel-oracle
npm install -g .
sentinel-oracleAfter cloning and installing globally, the sentinel-oracle command is available
from anywhere. The server starts and prints the dashboard URL in the terminal.
- Tailscale instalado en los 3 dispositivos (servidor, workstation, telefono)
- Los 3 dispositivos conectados al mismo tailnet (
tailscale status) - Node.js >= 20 en el servidor Oracle
- GitHub App creada y instalada en tu repositorio
1. Instalar Tailscale (si no lo tiene):
# En el servidor Oracle, workstation, y telefono:
# Descargar desde https://tailscale.com/download
tailscale up
tailscale status # Verificar que los 3 dispositivos aparecen2. Crear GitHub App (ver docs/github-app-setup.md para detalles):
- Vaya a
github.com/settings/apps/new - Nombre:
sentinel-oracle-tu-org - Permisos: Pull requests (Read & write), Checks (Read & write), Contents (Read)
- Genere private key → descarga archivo
.pem
3. Instalar la app en tu repositorio:
- En la pagina de la app → sidebar Install App → Install
- Seleccione su repositorio → Install
- Click engranaje ⚙️ al lado del repo instalado
- Anote el Installation ID de la URL:
settings/installations/<NUMERO>
4. Iniciar el servidor:
sentinel-oracle5. Configurar via web:
Abra https://{IP_TAILSCALE}:3443/setup y siga los pasos:
- Owner + Repository
- App ID + Installation ID + Private Key (pegue el contenido del .pem o la ruta)
- Opciones de scan
- Test connection → Save
6. Registrar el telefono:
En el telefono (con Tailscale conectado), abra la misma URL del dashboard. Click Register Device → biometria.
Listo. Los PRs abiertos apareceran en la cola. Para autorizar un merge: click Authorize → escanear QR con el telefono → biometria → merge.
sentinel-oracle Start the server (default)
sentinel-oracle start Start the server
sentinel-oracle scan Run a one-time security scan on the configured repository
sentinel-oracle --version, -v Print version
sentinel-oracle --help, -h Print helpSentinel Oracle includes a multi-layered security scanner that analyzes PR diffs across 14 intel modules. Scans are deduplicated by SHA-256 of PR sha + file metadata.
| Module | Analyzes |
|---|---|
| Capabilities | Filesystem, network, shell, dynamic code, database, crypto operations |
| Endpoints | URLs, IP addresses, external domains |
| Services | SDK integrations (Stripe, AWS, OpenAI, etc.) |
| Permissions | Workflow permission changes |
| Dependencies | npm, Python, Go, Rust dependency changes (EXPERIMENTAL: tarball diff) |
| Secrets | Environment variable exposure, hardcoded credentials |
| Trust | Data flow across trust boundaries |
| Crypto | Algorithm changes, key length changes |
| Auth | New routes, authentication middleware removal |
| Infrastructure | Docker, Kubernetes, Terraform changes |
| CI Integrity | Step redistribution, cache camouflage, fingerprint churn, synthetic telemetry, evasion signals, campaign detection |
| Trust Drift | New collaborators, GitHub Apps, secrets, runners, environments, branch protection removals, permission escalations |
| Security DNA | Capability fingerprint aggregator (14 dimensions) |
| Build Intelligence | Build surface, build chain, expected graph, trust engine, evidence graph, build story |
When enabled in Settings (toggle switch), all PRs are scanned automatically on queue refresh. Manual SCAN button appears when auto-scan is OFF. Scans are cached per PR SHA and never re-executed for identical code.
| Severity | Score Range | Examples |
|---|---|---|
| Critical | >=10 | Secrets, credential leaks, auth bypass |
| High | >=7 | Permission escalation, crypto weakness, CI anomalies |
| Medium | >=4 | New capabilities, external endpoints, campaign signals |
| Low | >=1 | Info-level findings, new dependencies |
| None | 0 | No issues |
Sentinel Oracle includes an AI-powered PR analysis engine that generates structured summaries, identifies architectural changes, flags security-relevant diffs, detects instruction manipulation attempts, and assigns review priorities.
Two AI backends are supported:
| Backend | Setup | Performance |
|---|---|---|
| Ollama (recommended) | Install Ollama, pull a model (ollama pull qwen2.5:1.5b) |
~2-10s per analysis |
| GGUF (local) | Download a .gguf file to ~/.sentinel/models/ |
~5-30s per analysis (via node-llama-cpp) |
The server auto-detects available models at /api/ai/models. When both backends are present, Ollama is preferred.
A dropdown in Settings > AI Intelligence lists all detected models. Select one explicitly, or leave it on auto for automatic detection.
- PR Summarization: Structured executive summary with architectural changes, dependencies, and reviewer notes
- Security-Relevant Change Detection: Flags files touching auth, secrets, permissions, and encryption
- Instruction Manipulation Detection: Scans diffs for prompt injection, hidden instructions, role redefinition, suppression attempts, and config manipulation
- Review Priority Assignment: Computes
reviewPriority(low/critical),impactLevel, andestimatedComplexityfrom file metadata and LLM output - Output Sanitization: All LLM output is sanitized server-side — markdown (bold, code blocks, links, HTML tags) is stripped before storage
GET /api/ai/status returns model availability, health status, and backend type. The health check verifies:
- For Ollama: runs
ollama show <model> - For GGUF: checks file existence on disk
When enabled in Settings, all PRs are automatically analyzed after scanning. Analyses are cached per PR SHA — identical PRs never re-trigger the LLM.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/ai/status |
None | AI backend status, health, available models |
| GET | /api/ai/models |
None | List detected models (Ollama + GGUF) |
| POST | /api/prs/:number/ai-analyze |
Cookie | Run AI analysis on a specific PR |
The CI Integrity engine monitors GitHub Actions workflows for anomalous behavior using three comparison windows (7-day, 30-day, full history) with MAD-based z-score computation.
- Step Redistribution: Detects workflow steps moving between jobs between commits
- Cache Camouflage: Detects cache key manipulation across commits
- Fingerprint Churn: Detects CI job structure changes between commits
- Synthetic Telemetry: Detects fake workflow events injected into the API
- Evasion Signals: Detects YAML anchors, merge tags, template variables used to obfuscate workflow changes
- Campaign Detection: Cross-PR weighted scoring (exec=10, escalation=8, runner=8, secret=6, capability=2 capped at 20, domain=1 capped at 10, endpoint=1 capped at 10)
Each check's baselines are computed independently for three windows:
- All history: No minimum sample requirement
- 30-day: Requires >=3 samples
- 7-day: Requires >=3 samples
An anomaly triggers when any window exceeds its z-score threshold (z>10=critical, z>5=high, else 10pts deduction). The worst z-score across all windows is used.
Starting from 100, deductions are applied per anomaly:
- Critical anomaly: -25 points
- High anomaly: -15 points
- Medium anomaly: -5 points
- Low anomaly: -1 point
- Z-score > 10: -40 points
- Z-score > 5: -20 points
- Z-score else: -10 points
- Missing sensor data: -10 points
Only PRs explicitly marked with trusted: true train the baseline model. Records without explicit trust are excluded when trustedOnly is enabled.
Trust Drift monitors the GitHub organization for changes that weaken the repository's security posture. Seven signals are tracked:
| Signal | Weight | What it detects |
|---|---|---|
| Collaborator | 2 | New users added with write/admin access |
| GitHub App | 3 | New GitHub Apps installed on the repo |
| Secret | 3 | New secrets added to environments |
| Runner | 3 | New self-hosted runners registered |
| Environment | 2 | New environments created |
| Branch Protection | 4 | Removal of branch protection rules |
| Permission Escalation | 4 | Escalated permissions in YAML workflow files |
Thresholds: >=10 = critical, >=6 = high, >=3 = medium
Security DNA is a capability aggregator that reads from existing IntelReport modules to produce a repository capability fingerprint. It is NOT a new detector.
| Capability | Description |
|---|---|
| filesystem | File read/write operations |
| network | Network requests, HTTP calls |
| shell | Command execution, subprocesses |
| dynamicCode | Eval, code generation |
| database | Database queries, migrations |
| crypto | Cryptography operations |
| secrets | Secret/hardcoded credential usage |
| runners | CI runner configuration changes |
| environments | Environment variable manipulation |
| collaborators | New collaborator additions |
| permissionEscalations | Workflow permission changes |
| newDomains | New external domains |
| newIntegrations | New service integrations |
| workflowCount | Number of workflow files |
GET /api/dna -- returns { current, history, changes, summary, snapshotCount }
Snapshots are stored in the capability_snapshots SQLite table, auto-generated after every scan.
Validated against 5 real open-source repositories (Kubernetes, Next.js, Home Assistant, OpenTelemetry Collector, Open WebUI). Produces differentiated fingerprints correlating with each project's technical domain.
Build Intelligence predicts what a PR would introduce to the build pipeline. Unlike static SAST rules, it analyzes the structural impact: tools, scripts, dependencies, and build chain changes — and predicts trust degradation.
PR Diff ──→ Build Surface ──→ Build Chain ──→ Expected Build Graph ──→ Trust Engine ──→ Evidence Graph ──→ Build Story
| Stage | Purpose |
|---|---|
| Build Surface | Detects tools, scripts, and dependencies from diff. Classifies shell exec, network access, file access per script. |
| Build Chain | Maps install→build→test→deploy stages. Detects deviations (e.g., preinstall with curl|bash). |
| Expected Build Graph | Predicts what the build graph would look like after merge. New nodes, removed nodes, modified edges. |
| Trust Engine | 7-dimension weighted scoring (0-100 per dimension). |
| Evidence Graph | SHA-256 linked evidence chain from diff analysis. |
| Build Story | Narrative summary of what this PR would cause in the build pipeline. |
| Dimension | Weight | What it measures |
|---|---|---|
| toolchain_identity | 18% | Recognized vs unknown build tools |
| input_identity | 18% | Source origin of build inputs |
| artifact_integrity | 15% | Hash/signature verification of outputs |
| behavior | 15% | Expected vs actual build behavior |
| network | 12% | Network access patterns during build |
| graph | 12% | Build graph structural integrity |
| trend | 10% | Historical trust trajectory |
- Preinstall/postinstall hooks with shell execution or network access
- Suspicious build scripts (obfuscated code, base64 payloads, eval patterns)
- Dependency changes (new packages from untrusted sources, GitHub refs)
- CI workflow modifications (new steps, changed runners, permission escalations)
- Build chain deviations (unexpected stages, out-of-order execution)
- Trust boundary crossings (req→exec, input→eval, req→db patterns)
| Field | Description |
|---|---|
| verdict | CLEAN / REVIEW / CRITICAL |
| trustScore | 0-100 overall trust score |
| risk | low / medium / high |
| buildSurface | Tools, scripts, dependencies with risk classification |
| buildChain | Expected flow, detected deviations |
| expectedGraph | New/removed/modified nodes |
| trust | 7-dimension breakdown with scores and weights |
| story | Narrative, events timeline, risk change |
| evidenceGraph | Nodes and edges from diff evidence |
The Scan History panel provides visual analytics over all scanned PRs:
- Trend Chart: Risk score over time with area fill
- Severity Distribution: Bar chart of critical/high/medium/low findings
- Per-PR Table: Sortable list with risk scores, finding counts, scan timestamps
- Filterable: By severity level, time range
Data is computed from the scan_results table and cached for performance.
The SOC panel provides a unified view of security findings across all scanned PRs.
- Average Risk Score: Computed across all scanned PRs
- Open Findings: Total unresolved security findings
- Critical Findings: Findings requiring immediate attention
- Pending PRs: PRs awaiting review or authorization
Security Inbox groups unresolved findings by severity level. Each finding links to its source PR and shows the file path and line number. Analysts can resolve or assign findings individually.
PRs are listed in a priority queue sorted by risk score. Each entry shows:
- Risk score badge (color-coded)
- Author, title, and file count
- Scan timestamp
- Quick action buttons (scan, view details, resolve)
Sentinel Oracle maintains a full inventory of tokens used by the repository: GitHub PATs, GitHub App installation tokens, OAuth tokens, and secrets found during file scanning.
| Type | Description |
|---|---|
github_pat |
Personal access tokens discovered via GitHub API |
github_app |
GitHub App installation tokens |
github_oauth |
OAuth tokens from integrations |
found_secret |
Secrets found during SAST scans (never stored raw) |
generic |
Other token types |
Each token is scored based on:
- Scope breadth: Wide scope = higher risk
- Age: Tokens not rotated in 90+ days
- Expiry status: Expired tokens tracked separately
- Fingerprint: SHA-256 hash (raw token is never stored)
Token drift detection monitors for:
- New tokens not previously seen
- Tokens that changed scopes
- Tokens that were revoked or expired
- Tokens nearing expiry (within 30 days)
GET /api/inventory/tokens/drift returns a drift report. The inventory.html page shows drift status with per-token risk badges and last-seen timestamps.
Sentinel Oracle includes an AI skills system designed for coding assistants (Cursor, Copilot, Claude Code, Codex, etc.) to query scan results programmatically.
| Skill | Purpose | Endpoint |
|---|---|---|
scan_results |
Query scan results by PR, status, or risk level | GET /api/skills/scan-results |
scan_pr_files |
Get file-level scan details for a PR | GET /api/skills/scan-pr-files/:prNumber |
scan_history |
Historical scan data with trend analysis | GET /api/skills/scan-history |
security_dna |
Security DNA snapshot for a repository | GET /api/skills/security-dna |
scan_stats |
Aggregate statistics across all scans | GET /api/skills/scan-stats |
Skills return structured JSON optimized for LLM context windows. Each response includes:
- Summary of findings with severity breakdown
- Top actionable items
- Historical context (trends, regressions)
The CI policy engine allows defining organizational rules for CI/CD integrity.
sentinel.policy.yml defines per-check and global rules:
global:
maxStepRedistribution: 3
maxCacheCamouflage: 2
notifyOnAnomaly: true
checks:
"build / test":
maxRedistribution: 5
requireBaseline: true
trustedOnly: true- Monitor (default): Logs violations but does not block merges
- Enforce: Fails the scan if policy thresholds are exceeded
GET /api/policy reads the current policy. POST /api/policy updates it.
An optional dashboard password adds a second authentication layer. When enabled, users must enter the password before accessing the dashboard.
- Password is stored as a SHA-256 hash (never plaintext)
- Strength indicator in real-time during password setup
- Forgot password flow available via
POST /api/config/password/reset
On first visit, a WebAuthn enrollment token is created with a 120-second TTL. The phone must scan the QR code within this window to register a passkey. Once registered, all future authorizations use the standard QR → biometric flow.
For environments without a phone, POST /api/auth/direct provides a browser-only WebAuthn flow using a platform authenticator (fingerprint or Face ID on the workstation itself).
The setup wizard (/setup) tracks configuration progress:
- GitHub token configured
- Repository owner and name set
- Scan enabled
- Auto-scan enabled
- AI enabled
Progress is persisted and shown on the main dashboard until all items are complete.
Tests are organized by intention:
| Directory | Intent | CI Behavior |
|---|---|---|
test/regression/ |
Must-pass tests verifying core functionality | FAIL on failure |
test/evasion/ |
Documented bypasses (attacker perspective) | PASS = no detection expected |
test/red-team/ |
Adversarial attack scenarios | PASS = detection confirmed |
test/integration/ |
Multi-layer integration (HTTP, DB, WebAuthn) | FAIL on failure |
These files test that Sentinel Oracle detects malicious code patterns. They import the scanner modules and verify detection works. They are not standalone exploits and pose no risk outside this repo. Security tools like SonarQube, Semgrep, and CodeQL follow the same pattern.
- Misión / Visión
- Quick Start (Global CLI)
- CLI Reference
- Security Scanner
- AI PR Intelligence
- CI Integrity Engine
- Trust Drift Detection
- Security DNA
- Security Operations Center (SOC)
- Token Inventory
- Dashboard Security
- Problem Statement
- Architecture
- Authorization Flow
- Cryptographic Protocol
- CSRF Protection
- WebAuthn Re-assertion
- Threat Model
- Installation
- Configuration
- Configuration Reference
- GitHub App Setup
- API Endpoints
- Database Schema
- Network Architecture
- Tailscale Integration
- Deployment
- Security Considerations
- Known Limitations
- Verification Checklist
- Environment and File Reference
- Troubleshooting
- Community
- License
Conventional CI/CD pipelines conflate development capability with deployment authority. 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:
- A malicious npm package achieves remote code execution on the workstation.
- A compromised VS Code extension exfiltrates a GitHub PAT from the local credential store.
- A phishing attack harvests a session token with merge scope.
In all cases, the attacker gains merge authority without additional authentication. Sentinel Oracle eliminates this vector by enforcing physical separation: the workstation never holds credentials with merge scope, the oracle server executes merges only after cryptographic verification, and the phone provides ephemeral biometric consent for each individual pull request.
Sentinel Oracle implements a three-device trust model with three physically independent devices connected via a zero-trust mesh network (Tailscale / WireGuard).
The developer's daily machine. Runs the IDE, browser, node_modules, and 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.
A dedicated physical device (Raspberry Pi 2W+, Intel NUC, thin client, old Android phone running Termux, or any Linux server) running the sentinel-oracle server.
Responsibilities:
- Polls GitHub for open PRs that have passed CI.
- Generates HMAC-SHA256 signed challenges with 45-second TTL.
- Verifies WebAuthn assertions against stored credentials.
- Executes merge operations via the GitHub API.
- Exposes an HTTPS dashboard bound to the Tailscale interface only.
No ports are open to the public internet.
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.
- Biometric verification (Face ID, fingerprint) unlocks the passkey.
- The phone 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. The phone never interacts with GitHub directly.
- Workstation opens the oracle dashboard in the browser.
- Oracle polls GitHub for open PRs that have passed CI checks.
- Oracle generates an HMAC-SHA256 challenge bound to the specific PR number (45-second TTL, one-time use).
- Oracle stores the challenge in SQLite.
- Oracle returns a QR payload to the workstation dashboard.
- Workstation displays the QR code on screen.
- Phone scans the QR code via the device camera.
- Phone parses the challenge payload (challenge ID, host, signature).
- WebAuthn biometric prompt appears on the phone.
- Phone signs the assertion with the passkey private key.
- Phone POSTs the signed assertion to
/api/authorizeon the oracle server.
- Oracle verifies the HMAC signature on the challenge (constant-time).
- Oracle verifies the WebAuthn assertion against the stored credential.
- Oracle verifies the PR number in the assertion matches the challenge.
- Oracle marks the challenge as consumed (atomic SQLite write).
- Oracle calls the GitHub merge API with the stored PAT.
- Oracle returns the merge result to the workstation dashboard.
Total round-trip: approximately 10-20 seconds (including human interaction). Critical window (scan to authorize): 40 seconds (WebAuthn timeout). Challenge TTL: 45 seconds.
All cryptographic operations use the Web Crypto API (crypto.subtle) on both
the server and client sides.
HMAC_KEY = HKDF-SHA256(master_secret, salt="sentinel-oracle-v1", info=server_fingerprint)
function GenerateChallenge(prNumber, sessionId):
nonce = crypto.getRandomValues(new Uint8Array(32))
payload = JSON.stringify({
pr: prNumber,
nonce: hex(nonce),
sessionId,
ttl: Date.now() + 45000
})
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 (environment variable ORACLE_MASTER_SECRET, minimum 32 bytes).
Each challenge is bound to exactly one PR number. The nonce ensures uniqueness. The TTL limits the replay window. The server rejects expired challenges.
challengeId = SHA256(payload + signature).slice(0, 16)
qrPayload = JSON.stringify({
v: 1,
cid: challengeId,
sig: hex(signature),
host: "https://100.x.y.z:3443"
})
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 WebAuthn challenge field is set to challengeId (derived from the
HMAC-signed challenge payload). This creates 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).
assertion = await navigator.credentials.get({
publicKey: {
challenge: new Uint8Array(challengeId),
allowCredentials: [{ id: credentialId, type: "public-key" }],
userVerification: "required",
timeout: 40000
}
})The server performs the following checks in order before executing a merge:
- Challenge lookup by
challengeId-- rejects if not found. - Challenge consumption check -- rejects if already used.
- Challenge expiry check -- rejects if TTL exceeded.
- HMAC signature verification -- constant-time comparison.
- WebAuthn assertion verification -- ECDSA P-256 signature, RP ID hash, origin, user presence, signature counter.
- PR number binding verification -- the PR in the challenge payload must match the PR in the request.
- Challenge consumption (atomic DB write) -- single-use gating.
- GitHub merge API call via Octokit -- squash merge method.
Steps 1-6 are designed such that any single failure aborts the operation with no state mutation. Challenge consumption (step 7) happens before the GitHub API call to prevent race conditions on retry.
Every mutating API endpoint (POST, PUT, DELETE) requires a per-session CSRF
token. The token is generated at session creation and returned via
GET /api/session/csrf-token. Frontend requests must include the token in the
X-CSRF-Token header:
POST /api/lockdown
Cookie: sentinel_session=<sid>
X-CSRF-Token: <token>
Content-Type: application/json
{ "reAssertToken": "..." }Without a valid CSRF token, the server returns 403.
Sensitive actions (lockdown, unlock, device revocation, PR rejection) require fresh biometric confirmation via WebAuthn re-assertion in addition to the session cookie and CSRF token.
The re-assertion flow:
- Client calls
POST /api/auth/re-assertwith{ action: "lockdown" } - Server generates a WebAuthn assertion challenge and stores it in the config table
- Client calls
navigator.credentials.get()with the challenge - Client sends the assertion to
POST /api/auth/re-assert/complete - Server verifies the assertion and returns a one-time
reAssertToken(60-second TTL) - Client includes the
reAssertTokenin the sensitive action request body
This ensures that even with an active session, an attacker cannot lock down the system, revoke devices, or reject PRs without physical access to a registered device.
| Vector | Risk | Controls | Residual Risk |
|---|---|---|---|
| Workstation RCE | Assets: dashboard session (read-only) | No merge credentials on workstation; dashboard is read-only | Attacker can display fake QR codes but cannot complete authorization without phone biometric |
| Phone theft | Assets: WebAuthn passkey private key | Passkey is biometric-bound; device PIN required after restart | Attacker with device unlock and live biometric could authorize merges within 45s window |
| Oracle physical theft | Assets: GitHub PAT, HMAC secret, credentials | Full-disk encryption; BIOS password; secure boot | Attacker with unlimited physical access and FDE passphrase can extract all secrets |
| Oracle remote compromise | Assets: all merge authority | Minimal OS; Tailscale ACLs; no writable endpoints without auth | Zero-day in Node.js or Tailscale daemon |
| Network MITM | Assets: challenge, assertion in transit | Tailscale WireGuard encryption; self-signed TLS; 45s TTL | Attacker on the tailnet node itself |
| Replay attack | Assets: re-use of captured challenge | Single-use (atomic DB); 45s TTL; PR binding | Zero |
| QR phishing | Assets: assertion to attacker server | User must verify PR number visually; host field in QR | User error |
When lockdown is activated (via the dashboard or a physical button on the oracle server), the server immediately:
- Invalidates all pending challenges in the database.
- Sets all open PRs to a blocked/failure commit status.
- Rejects all new challenge generation requests.
- Persists the lockdown flag to disk.
Lockdown persists across server restarts. Deactivation requires physical access to the oracle server.
git clone https://github.com/javier20dev25/sentinel-oracle.git
cd sentinel-oracle
npm install
npm run build- Node.js >= 20
- Tailscale (recommended) or direct LAN connectivity
- WebAuthn-compatible phone browser (Chrome, Safari, Edge)
- GitHub Personal Access Token with
pull-requests: writescope
| Device | Power | Notes |
|---|---|---|
| Raspberry Pi 2W+ | ~5W | Recommended. Always-on, low power, silent. |
| Intel NUC / thin client | ~10-15W | x86 compatible, more CPU for larger repos. |
| Old Android phone | ~2-5W | Termux + Node.js. Built-in UPS (battery). Zero e-waste. |
| Linux VPS | Varies | Requires Tailscale. Only outbound to GitHub API. |
| Windows PC | ~50-100W | NSSM for background service. |
| macOS | ~10-30W | LaunchAgent for auto-start. |
# Configure environment
export GITHUB_TOKEN="github_pat_..."
export ORACLE_MASTER_SECRET="$(openssl rand -hex 32)"
# Create config
mkdir -p ~/.sentinel-oracle
# Edit ~/.sentinel-oracle/config.json with your repository details
# Start the server
npm startThe server listens on https://<tailscale-ip>:3443. Open the dashboard in
your phone browser via Tailscale, register a passkey, and authorize merges
with your biometric.
npm run setupThe setup script walks through each dependency:
- Node.js version check (requires >= 20).
- TLS certificate generation (self-signed if missing).
- Tailscale detection and optional configuration.
- Config file validation at
~/.sentinel-oracle/config.json.
Create ~/.sentinel-oracle/config.json:
{
"githubToken": "github_pat_...",
"repoOwner": "your-org",
"repoName": "your-repo",
"bindAddress": "100.x.y.z",
"rpId": "100.x.y.z",
"serverOrigin": "https://100.x.y.z:3443",
"port": 3443,
"tlsCert": "./cert.pem",
"tlsKey": "./key.pem",
"locked": false
}Alternatively, use environment variables for secrets:
export GITHUB_TOKEN="github_pat_..."
export ORACLE_MASTER_SECRET="$(openssl rand -hex 32)"
| Field | Type | Default | Description |
|---|---|---|---|
port |
number | 3443 |
HTTPS listen port. Use >= 1024 on Linux to avoid root. |
host |
string | "0.0.0.0" |
Bind address (all interfaces). Change to specific IP for strict binding. |
bindAddress |
string | auto | IP for QR URLs and WebAuthn origin. Auto-detection: Tailscale Funnel URL > Tailscale IP (100.x.x.x) > first non-loopback IPv4. |
dataDir |
string | ~/.sentinel-oracle |
Directory for SQLite database, encryption key, TLS certs, config. |
githubToken |
string | "" |
GitHub PAT with pull-requests:write scope. Required. |
githubOwner |
string | "" |
GitHub organization or user that owns the target repository. |
githubRepo |
string | "" |
Repository name (without owner prefix). |
githubStatusContext |
string | "Sentinel Authorization" |
Commit status context name. Must match the required status check in branch protection. |
serverOrigin |
string | auto | Origin URL for WebAuthn Relying Party. Auto-assembled from bindAddress + port. |
rpId |
string | auto | WebAuthn Relying Party ID (domain without port). Auto-assembled from bindAddress. |
challengeTtlMs |
number | 45000 |
QR challenge TTL in milliseconds. |
rateLimitAuth |
number | 5 |
Maximum authorization attempts per rateLimitWindowMs. |
rateLimitWindowMs |
number | 60000 |
Rate limit window in milliseconds. |
encryptionKey |
Buffer | auto | AES-256 key for HMAC signing key derivation. Generated once. DO NOT SET IN CONFIG. |
approveReasonRequired |
boolean | false |
If true, /confirm requires non-empty "reason" field. |
locked |
boolean | false |
Emergency lockdown. Persisted state. |
passwordHash |
string | "" |
SHA-256 hash of dashboard password. Empty = no password. |
enrollmentTokenTtlMs |
number | 120000 |
First-time device enrollment token TTL. |
githubAppId |
string | "" |
GitHub App ID for JWT authentication |
githubInstallationId |
string | "" |
GitHub App installation ID |
githubPrivateKeyPath |
string | "" |
Path to GitHub App private key PEM file |
githubWebhookSecret |
string | "" |
Secret for verifying GitHub webhook payloads |
aiEnabled |
boolean | false |
Enable AI-powered PR analysis |
aiModel |
string | "auto" |
AI model selection: auto, ollama:qwen2.5:1.5b, gguf:... |
autoScan |
boolean | false |
Auto-scan all PRs on queue refresh |
autoAnalyze |
boolean | false |
Auto-run AI analysis after scan |
securityInbox |
boolean | false |
Enable Security Inbox in SOC panel |
analystQueue |
boolean | false |
Enable Analyst Queue in SOC panel |
scanEnabled |
boolean | false |
Master toggle for the security scanner |
Sentinel Oracle supports two authentication modes for GitHub API access:
- Personal Access Token (PAT) — The traditional approach. A fine-grained PAT with
pull-requests: writescope is stored on the oracle server. Simple to set up but creates a long-lived credential. - GitHub App (recommended) — Installation tokens with 1-hour TTL, auto-refresh, and repository-scoped permissions. No long-lived credentials are stored on the oracle server.
| Aspect | PAT | GitHub App |
|---|---|---|
| Token lifetime | Long-lived (user-managed rotation) | 1 hour TTL, auto-refreshed |
| Scope | User-scoped (all repos the user can access) | Repository-scoped (specific repos only) |
| Risk profile | High — token exfiltration grants wide access | Low — compromised token expires within 1 hour |
| Rotation | Manual, periodic | Automatic, every hour |
Using a GitHub App eliminates the risk of a long-lived PAT being exfiltrated from the oracle server. Even if an attacker gains access to the server, the installation token expires within 60 minutes and is scoped to a single repository.
See docs/github-app-setup.md for complete setup instructions, including registering a GitHub App, installing it on your organization, generating a private key, and configuring the required environment variables.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/status |
None | Server status, uptime, locked state, setup required |
| GET | /api/session/check |
Cookie | Session validation. Returns { authenticated, deviceName } |
| GET | /api/session/csrf-token |
Cookie | Returns CSRF token for mutating requests |
| POST | /api/session/logout |
Cookie | Destroy current session |
| POST | /api/webauthn/register/begin |
Cookie | Start WebAuthn credential registration |
| POST | /api/webauthn/register/complete |
Cookie | Complete WebAuthn credential registration |
| POST | /api/webauthn/assert/begin |
None | Start WebAuthn assertion (login) |
| POST | /api/webauthn/assert/complete |
None | Complete WebAuthn assertion (login) |
| POST | /api/auth/re-assert |
Cookie | Generate re-assertion challenge for sensitive actions |
| POST | /api/auth/re-assert/complete |
Cookie | Verify re-assertion, returns one-time token |
| GET | /api/prs |
Cookie | List pending PRs (triggers poll) |
| GET | /api/prs/history |
Cookie | List completed/authorized PRs |
| POST | /api/prs/:number/authorize |
Cookie | Initiate authorization, returns QR challenge |
| POST | /api/prs/:number/confirm |
Cookie | Confirm with WebAuthn assertion, executes merge |
| POST | /api/prs/:number/reject |
Cookie+CSRF+RA | Reject authorization (needs re-assertion token) |
| POST | /api/prs/:number/scan |
Cookie | Trigger SAST scan on PR files |
| GET | /api/prs/:number/checks |
Cookie | Check run results for a PR |
| GET | /api/prs/:number/file-history/:filename |
Cookie | File modification history + CI duration chart data |
| POST | /api/lockdown |
Cookie+CSRF+RA | Emergency lockdown (needs re-assertion token) |
| POST | /api/unlock |
Cookie+CSRF+RA | Deactivate lockdown (needs re-assertion token) |
| GET | /api/devices |
Cookie | List registered WebAuthn credentials |
| POST | /api/devices/:credentialId/revoke |
Cookie+CSRF+RA | Revoke a device (needs re-assertion token) |
| GET | /api/audit |
Cookie | Authorization audit log |
| GET | /api/metrics |
Cookie | Aggregated metrics and per-author stats |
| GET | /api/status/branch-protection |
Cookie | Branch protection status for main |
| POST | /api/admin/backfill-history |
Cookie | Backfill historical PR data for charts |
| GET | /api/admin/backfill-status |
Cookie | Backfill progress (current/total/done) |
| POST | /api/webhook/github |
HMAC | GitHub webhook receiver |
| GET | /api/inventory/tokens |
Cookie | Token inventory listing |
| POST | /api/inventory/tokens/scan |
Cookie | Scan repo for leaked tokens |
| GET | /api/ai/status |
None | AI backend status, health, model info |
| GET | /api/ai/models |
None | List detected AI models (Ollama + GGUF) |
| POST | /api/prs/:number/ai-analyze |
Cookie | Run AI PR intelligence analysis |
| POST | /api/prs/:number/ai-scan-analyze |
Cookie | Combined scan + AI analysis |
| POST | /api/prs/:number/ai-explain |
Cookie | Generate natural language explanation of findings |
| POST | /api/prs/:number/ai-scan-explain |
Cookie | Combined scan + explain in one call |
| POST | /api/prs/:number/ai-explain/save |
Cookie | Save AI explanation as SARIF-compatible report |
| GET | /api/prs/:number/ai-explain/saved |
Cookie | Retrieve saved explanation for a PR |
| POST | /api/prs/:number/blacklist |
Cookie+CSRF | Add PR to blacklist (skips scanning) |
| DELETE | /api/prs/:number/blacklist |
Cookie+CSRF | Remove PR from blacklist |
| GET | /api/prs/:number/scan-log |
Cookie | Scan processing log with timestamps |
| POST | /api/prs/:number/diff |
Cookie | Raw GitHub diff with optional base commit |
| GET | /api/workflow/telemetry |
Cookie | CI workflow telemetry (durations, failures, flakiness) |
| POST | /api/workflow/telemetry |
Cookie | Ingest new workflow telemetry step |
| POST | /api/workflow/telemetry/steps |
Cookie | Ingest multiple workflow steps at once |
| GET | /api/workflow/baselines |
Cookie | Computed workflow baselines for all checks |
| GET | /api/prs/:number/workflow-intel |
Cookie | Workflow intelligence report for a specific PR |
| GET | /api/prs/:number/ci-integrity |
Cookie | CI integrity score and detection results |
| GET | /api/policy |
Cookie | Read current CI policy configuration |
| POST | /api/policy |
Cookie | Update CI policy (z-score thresholds, rules, notifications) |
| GET | /api/dna |
Cookie | Security DNA snapshot (14-dimension capability fingerprint) |
| POST | /api/dna/compare |
Cookie | Compare current DNA against previous snapshots |
| POST | /api/dna/fingerprint/:prNumber |
Cookie | Generate DNA fingerprint for a specific PR |
| POST | /api/ai/compare |
Cookie | Compare AI analysis across two PRs |
| POST | /api/ai/batch |
Cookie | Batch-analyze multiple PRs simultaneously |
| GET | /api/inventory/tokens |
Cookie | Full token inventory (PATs, apps, found secrets) |
| POST | /api/inventory/tokens/scan |
Cookie | Scan repository for leaked tokens |
| GET | /api/inventory/tokens/stats |
Cookie | Token risk statistics (expired, near-expiry, scopes) |
| POST | /api/inventory/tokens/:id/refresh |
Cookie | Re-verify a specific token's validity |
| DELETE | /api/inventory/tokens/:id |
Cookie | Remove a token from inventory |
| GET | /api/inventory/tokens/drift |
Cookie | Token drift detection (changed scopes, new tokens, revoked tokens) |
| GET | /api/inventory/drift |
Cookie | Full inventory drift report |
| POST | /api/inventory/diff |
Cookie | Diff two inventory snapshots |
| GET | /api/soc/incidents |
Cookie | Security incidents requiring attention |
| POST | /api/soc/incidents/:id/resolve |
Cookie | Resolve a security incident |
| POST | /api/soc/incidents/:id/assign |
Cookie | Assign an incident to an analyst |
| GET | /api/analytics/export |
Cookie | Export analytics data (JSON or CSV format) |
| GET | /api/queue/stats |
Cookie | Analyst queue statistics (backlog, throughput, SLA) |
| GET | /api/blacklist |
Cookie | List all blacklisted PRs |
| GET | /api/config/settings |
Cookie | Current server settings |
| POST | /api/config/settings |
Cookie | Update server settings (scan, autoScan, AI, etc.) |
| GET | /api/config/password |
Cookie | Password status (enabled/disabled) |
| POST | /api/config/password |
Cookie | Set or change dashboard password |
| POST | /api/config/password/reset |
Cookie | Reset forgot password |
| GET | /api/config/github-status |
Cookie | GitHub connection status (owner, repo, token type) |
| POST | /api/config/github |
Cookie | Update GitHub configuration (owner, repo, token) |
| POST | /api/config/webhook |
Cookie | Update webhook configuration |
| POST | /api/config/lockdown |
Cookie+CSRF+RA | Toggle lockdown (no re-assert required for unlock) |
| POST | /api/admin/orphan-scans |
Cookie | Cleanup orphan scan results |
| POST | /api/admin/backfill-history |
Cookie | Backfill historical PR data for charts |
| GET | /api/admin/backfill-status |
Cookie | Backfill progress (current/total/done) |
| POST | /api/setup/begin |
None | Start first-time setup (creates initial session) |
| POST | /api/setup/complete |
None | Complete setup (save owner, repo, token, settings) |
| POST | /api/prs/:number/scan |
Cookie | Trigger SAST scan on a specific PR |
| GET | /api/prs/:number/scan-result |
Cookie | Cached SAST scan result for a PR |
| POST | /api/github/sync-app-token |
Cookie | Sync GitHub App token (force refresh) |
| GET | /api/github/token-info |
Cookie | Current GitHub token info (type, scopes, expiry) |
| GET | /api/installer/sentinel-telemetry |
None | Installer telemetry data |
Auth key: Cookie = sentinel_session cookie. CSRF = X-CSRF-Token header. RA = reAssertToken in request body.
Request:
{ "prNumber": 142 }Response:
{
"challengeId": "uuid",
"qrDataUrl": "data:image/png;base64,...",
"prNumber": 142,
"expiresAt": "2026-06-06T..."
}Request:
{
"challengeId": "uuid",
"credential": { "id": "...", "response": { ... }, "clientExtensionResults": {} },
"challenge": "webauthn-server-challenge",
"reason": "optional reason string"
}Response:
{
"authorized": true,
"prNumber": 142,
"merged": true
}{ "reason": "Compromised workstation reported" }Rejects all pending challenges, sets all open PRs to failure commit status, persists locked state to SQLite.
Returns current branch protection status for the main branch:
- Checks whether required status checks are configured.
- Verifies admin enforcement is enabled.
- Detects force push settings.
- Returns a JSON summary of each check and an overall pass/fail status.
Returns detailed check run results for a specific PR:
{
"prNumber": 142,
"checks": [
{
"name": "CI / test (18.x)",
"conclusion": "success",
"status": "completed",
"startedAt": "2026-06-10T10:00:00Z",
"completedAt": "2026-06-10T10:02:30Z",
"durationMs": 150000
}
],
"diff": {
"filesChanged": 12,
"additions": 45,
"deletions": 8
}
}Returns aggregated metrics for audit and analysis:
{
"summary": {
"total": 150,
"pending": 3,
"authorized": 120,
"rejected": 20,
"expired": 7
},
"mergeTimes": [
{
"prNumber": 142,
"requestedAt": "2026-06-10T10:00:00Z",
"authorizedAt": "2026-06-10T10:02:00Z",
"approvedAt": "2026-06-10T10:02:30Z",
"approvalWaitMs": 120000,
"totalDurationMs": 150000
}
],
"perAuthor": [
{
"author": "javier20dev25",
"mergedCount": 45,
"rejectionCount": 5,
"averageWaitMs": 95000
}
]
}GitHub webhook receiver. Accepts pull_request and push events.
Used for:
- Real-time PR notifications when new pull requests are opened or updated.
- Unauthorized merge detection — triggers alerts when merges bypass Oracle.
Requires Content-Type: application/json header.
If githubWebhookSecret is configured, Oracle verifies the X-Hub-Signature-256 header to authenticate the webhook payload.
SQLite database at {dataDir}/oracle.db. Three tables:
| Column | Type | Description |
|---|---|---|
| id | TEXT | UUID v4 challenge identifier |
| pr_number | INTEGER | Target PR number |
| signature | TEXT | HMAC-SHA256 signature |
| expires_at | INTEGER | Unix ms timestamp, challenge invalid after |
| consumed | INTEGER | 0 or 1. One-time consumption flag |
| created_at | TEXT | ISO 8601 timestamp |
| Column | Type | Description |
|---|---|---|
| id | INTEGER | Auto-increment primary key |
| name | TEXT | Human-readable label (e.g., "Pixel 7") |
| credential_id | TEXT | WebAuthn credential ID (base64url, unique) |
| public_key | TEXT | COSE-encoded ECDSA P-256 public key |
| counter | INTEGER | WebAuthn signature counter |
| transports | TEXT | JSON array of authenticator transports |
| created_at | INTEGER | Unix ms registration timestamp |
| last_used_at | INTEGER | Nullable. Unix ms last authorization |
| Column | Type | Description |
|---|---|---|
| id | TEXT | UUID v4 session identifier |
| credential_id | TEXT | WebAuthn credential that created this session |
| device_name | TEXT | Human-readable device label |
| created_at | INTEGER | Unix ms creation timestamp |
| expires_at | INTEGER | Unix ms expiry (24h from creation) |
| last_used_at | INTEGER | Unix ms last activity (idle timeout 30min) |
| csrf_token | TEXT | Per-session CSRF token (32 byte hex) |
| user_agent | TEXT | Browser user agent at session creation |
| Column | Type | Description |
|---|---|---|
| id | INTEGER | Auto-increment primary key |
| timestamp | INTEGER | Unix ms event timestamp |
| action | TEXT | Event type: challenge_created, authorization_granted, authorization_rejected, merge_executed, merge_failed, lockdown_activated, lockdown_deactivated, device_registered, device_revoked, backfill, etc. |
| pr_number | INTEGER | Nullable. Target PR number |
| detail | TEXT | Free-text detail or JSON metadata |
| Column | Type | Description |
|---|---|---|
| id | INTEGER | Auto-increment primary key |
| pr_number | INTEGER | Unique PR number |
| owner | TEXT | GitHub owner |
| repo | TEXT | GitHub repo |
| title | TEXT | PR title |
| author | TEXT | PR author login |
| sha | TEXT | HEAD commit SHA |
| ci_status | TEXT | CI pass/fail/pending |
| sentinel_status | TEXT | Sentinel scan result |
| auth_status | TEXT | pending/authorized/rejected/expired |
| created_at | INTEGER | Unix ms PR creation |
| authorized_at | INTEGER | Nullable. Unix ms authorization |
| device_name | TEXT | Device that authorized |
| check_run_id | INTEGER | GitHub check run ID |
| Column | Type | Description |
|---|---|---|
| id | INTEGER | Auto-increment primary key |
| pr_number | INTEGER | PR number |
| filename | TEXT | File path |
| sha | TEXT | Commit SHA |
| status | TEXT | added/modified/removed |
| additions | INTEGER | Lines added |
| deletions | INTEGER | Lines removed |
| auth_status | TEXT | pending/authorized/rejected |
| scanned_at | INTEGER | Unix ms scan timestamp |
| Column | Type | Description |
|---|---|---|
| id | INTEGER | Auto-increment primary key |
| filename | TEXT | File path |
| sha | TEXT | Commit SHA |
| pr_number | INTEGER | PR number |
| check_name | TEXT | CI check name |
| duration_ms | INTEGER | Check duration in ms |
| scanned_at | INTEGER | Unix ms when fetched |
| Column | Type | Description |
|---|---|---|
| id | INTEGER | Auto-increment primary key |
| token_type | TEXT | github_pat / github_app / github_oauth / generic / found_secret |
| name | TEXT | Token name/label |
| source | TEXT | github_api / repo_scan / manual |
| scopes | TEXT | Comma-separated scopes |
| fingerprint | TEXT | SHA256 fingerprint (never stores raw token) |
| first_seen_at | INTEGER | Unix ms discovery |
| last_seen_at | INTEGER | Nullable. Unix ms last verification |
| expires_at | INTEGER | Nullable. Unix ms token expiry |
| last_rotation | INTEGER | Nullable. Unix ms last rotation |
| risk_score | TEXT | low / medium / high / critical |
| notes | TEXT | Free-text notes |
| metadata | TEXT | JSON blob |
| Column | Type | Description |
|---|---|---|
| key | TEXT | Unique config key |
| value | TEXT | Config value |
| Column | Type | Description |
|---|---|---|
| id | INTEGER | Auto-increment primary key |
| pr_number | INTEGER | PR number |
| sha | TEXT | Commit SHA |
| risk_score | INTEGER | 0-100 risk score |
| findings_count | INTEGER | Total findings |
| critical_count | INTEGER | Critical findings |
| high_count | INTEGER | High findings |
| medium_count | INTEGER | Medium findings |
| low_count | INTEGER | Low findings |
| scan_json | TEXT | Full scan result (JSON) |
| build_intel_json | TEXT | Build Intelligence result (JSON) |
| scanned_at | INTEGER | Unix ms scan timestamp |
| Column | Type | Description |
|---|---|---|
| id | INTEGER | Auto-increment primary key |
| pr_number | INTEGER | PR number |
| sha | TEXT | Commit SHA |
| analysis_json | TEXT | Full AI analysis result (JSON) |
| review_priority | TEXT | low / medium / high / critical |
| injection_detected | INTEGER | 0 or 1, prompt injection flag |
| analyzed_at | INTEGER | Unix ms analysis timestamp |
| Column | Type | Description |
|---|---|---|
| id | INTEGER | Auto-increment primary key |
| sha | TEXT | Commit SHA at time of snapshot |
| snapshot_json | TEXT | 14-dimension capability fingerprint (JSON) |
| created_at | INTEGER | Unix ms snapshot timestamp |
Oracle uses three discovery methods in order:
-
Tailscale Funnel (highest priority): Reads
tailscale funnel status --jsonfor existing HTTPS proxy configurations. If a proxy targets Oracle's port, the MagicDNS hostname is used. Valid HTTPS from Let's Encrypt. -
Tailscale IP (medium priority): Scans network interfaces for the Tailscale virtual adapter (100.64.0.0/10 range). Stable across reboots. Encrypted mesh connectivity.
-
LAN IP (fallback): First non-loopback IPv4 interface. May select Docker, VPN, or VirtualBox adapters on machines with multiple active interfaces. Set
bindAddressexplicitly if auto-detection selects the wrong address.
Phone (Tailscale app)
|
|-- Tailscale encrypted WireGuard tunnel
|
Oracle Server (100.x.y.z:3443)
|
|-- HTTPS (outbound, port 443)
|
GitHub API (api.github.com)
No ports are open to the public internet. All traffic flows through Tailscale's encrypted mesh.
| Source | Destination | Protocol | Description |
|---|---|---|---|
| Workstation | Oracle | HTTPS | GET /api/dashboard, /api/status |
| Phone | Oracle | HTTPS | POST /api/authorize (WebAuthn assertion) |
| Oracle | GitHub | HTTPS | POST /repos/:owner/:repo/pulls/:number/merge |
| Oracle | Phone | HTTPS | Response to /api/authorize |
| Oracle | Workstation | HTTPS | Response to /api/dashboard (QR + status) |
| Phone | Workstation | NONE | QR is optical, out-of-band |
The LAN-only model is the most secure -- the server is not exposed to the internet at all. However, the phone must be on the same network to scan the QR, and the self-signed TLS certificate produces a browser warning.
Tailscale solves both without opening ports:
-
Valid HTTPS:
tailscale serve --bg 3443provisions a Let's Encrypt certificate automatically. No browser warnings on the phone. -
Remote access: the phone can authorize from anywhere (4G, another office) via the encrypted Tailscale network. No port forwarding needed.
# On each device:
sudo tailscale up --authkey tskey-xxxx
# On the oracle server (optional HTTPS proxy):
sudo tailscale serve --bg 3443
# Verify connectivity:
tailscale status
curl -k https://100.1.2.5:3443/api/statusTailscale does NOT expose Oracle publicly. Traffic flows through Tailscale's encrypted WireGuard mesh, not through the public internet.
Phone -- Tailscale encrypted tunnel -- Oracle (no open ports)
Cloudflare Tunnel (explicitly not used) would expose Oracle to the public internet, expanding the attack surface. Tailscale keeps the server private.
| Mode | Risk | Reach | Status |
|---|---|---|---|
| LAN | Lowest | Same network | MVP (default) |
| VPN (Tailscale/WireGuard) | Low | Anywhere | v1.1 (auto-detected) |
| Public tunnel | Higher | Anywhere | Not planned |
# Install Node.js 20+
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs git
# Install Tailscale
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
# Clone and install
git clone https://github.com/javier20dev25/sentinel-oracle.git
cd sentinel-oracle
npm install && npm run build
# Configure secrets
export GITHUB_TOKEN="github_pat_..."
export ORACLE_MASTER_SECRET="$(openssl rand -hex 32)"
# Set up systemd service
sudo tee /etc/systemd/system/sentinel-oracle.service << 'EOF'
[Unit]
Description=Sentinel Oracle Merge Authorization Server
After=network.target tailscaled.service
Wants=tailscaled.service
[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/sentinel-oracle
ExecStart=/usr/bin/node dist/index.js
Restart=on-failure
RestartSec=10
Environment=NODE_ENV=production
Environment=GITHUB_TOKEN=...
Environment=ORACLE_MASTER_SECRET=...
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now sentinel-oracle# Install Node.js from https://nodejs.org (>= 20 LTS)
# Install Tailscale
winget install Tailscale.Tailscale
tailscale up
# Clone and install
git clone https://github.com/javier20dev25/sentinel-oracle.git
cd sentinel-oracle
npm install && npm run build
# Set environment variables
$env:GITHUB_TOKEN="github_pat_..."
$env:ORACLE_MASTER_SECRET="..."
# Run
npm start
# Optional: Windows service with NSSM
nssm install SentinelOracle "C:\Program Files\nodejs\node.exe" "dist\index.js"
nssm set SentinelOracle AppDirectory "C:\Users\you\sentinel-oracle"
nssm set SentinelOracle Start SERVICE_AUTO_START
nssm start SentinelOracle# Install Node.js via Homebrew
brew install node@22
# Install Tailscale
brew install --cask tailscale
# Clone and install
git clone https://github.com/javier20dev25/sentinel-oracle.git
cd sentinel-oracle
npm install && npm run build
# Run
npm start
# Optional: LaunchAgent for auto-start# Install Termux from F-Droid (not Play Store)
pkg update && pkg upgrade
pkg install nodejs git tailscale
# Clone and install
git clone https://github.com/javier20dev25/sentinel-oracle.git
cd sentinel-oracle
npm install && npm run build
# Keep the phone plugged in
# Use termux-wake-lock to prevent CPU sleep
termux-wake-lock
export GITHUB_TOKEN="github_pat_..."
export ORACLE_MASTER_SECRET="..."
npm startThe oracle server is the single trusted component. It should be:
- Located in a locked room or locked enclosure.
- Equipped with full-disk encryption (LUKS or BitLocker).
- Configured with BIOS/UEFI password and Secure Boot.
- Running no other workloads.
- The GitHub PAT should be a fine-grained token with
pull-requests: writescope only. Rotate every 90 days. ORACLE_MASTER_SECRETmust have at least 256 bits of entropy.- The encryption key is auto-generated at startup and stored at
{dataDir}/.encryption_keywith0o600permissions. - Rotate the master secret every 6 months.
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.
sudo timedatectl set-ntp true
sudo timedatectl statusAll merge authorization events are logged with: timestamp, PR number, challenge ID, credential ID, client IP (Tailscale IP), and result (success/failure/denied). Forward logs to a centralized system for alerting and forensic analysis.
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.
When using GitHub App authentication, no long-lived credentials are stored on the oracle server. Installation tokens are generated on-demand with a 1-hour TTL and are scoped to a single repository. This eliminates the risk of PAT exfiltration and reduces the blast radius of a server compromise.
The optional githubWebhookSecret configures HMAC-SHA256 verification of incoming webhook payloads. Oracle verifies the X-Hub-Signature-256 header against the request body, ensuring that only legitimate GitHub webhook events are processed. This prevents spoofed webhook deliveries from triggering unauthorized actions.
Oracle automatically verifies branch protection settings on the main branch before processing merge requests. It checks for required status checks, admin enforcement, and force push settings. If branch protection is misconfigured, Oracle logs a warning and returns the status via the /api/status/branch-protection endpoint.
- Branch protection rules must be configured independently on GitHub. Oracle's authorization flow is bypassed if administrators can push directly or force-push.
- PAT with merge permissions is stored on the oracle server. Network segmentation and TPM-backed storage reduce this risk.
- WebAuthn credential storage and encryption key share the same SQLite database. A full disk compromise yields both.
- LAN IP auto-detection may select a Docker, VPN, or VirtualBox interface
if the machine has multiple active network adapters. Set
bindAddressexplicitly in config.json if this occurs. - Branch protection verification is read-only — Oracle detects issues but does not auto-fix them.
- Webhook delivery is not guaranteed — polling serves as fallback.
- Unauthorized merge detection relies on webhook availability.
- 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).
- 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.
- NTP is enabled on the oracle server.
| Path | Purpose | Auto-created |
|---|---|---|
~/.sentinel-oracle/config.json |
User configuration | No (defaults used if absent) |
~/.sentinel-oracle/.encryption_key |
AES-256 HMAC signing key (32 bytes) | Yes |
~/.sentinel-oracle/oracle.db |
SQLite database | Yes |
~/.sentinel-oracle/server.key |
TLS private key | Yes (by setup script) |
~/.sentinel-oracle/server.cert |
TLS certificate (self-signed) | Yes (by setup script) |
./scripts/setup.cjs |
Interactive setup wizard | Part of repo |
./start.cmd |
Windows start shortcut | Part of repo |
- Verify phone is connected to Tailscale and the same tailnet.
- Run
tailscale statuson the oracle server to confirm all three devices are visible. - Check ACLs in the Tailscale admin console.
- Verify the oracle server is listening:
curl -k https://localhost:3443/health. - From the phone browser:
http://{tailscale-ip}:3443/health.
- Recommended: Install Tailscale on the phone and use
tailscale serve --bg 3443. - Alternative: Add the self-signed certificate to the phone's trust store.
- Not recommended: Disable TLS.
- Ensure the phone browser supports WebAuthn (Chrome, Safari, Edge).
- Check that
rpIdmatches the domain/IP the phone uses to reach the server. - Verify
serverOriginis a valid origin. - If using a self-signed certificate, WebAuthn may reject on some browsers. Use Tailscale for valid HTTPS.
node --version # Must be >= 20
npm run build # Check for TypeScript errors
node --stack-trace-limit=100 dist/index.js
node -e "console.log(JSON.parse(require('fs').readFileSync(require('os').homedir() + '/.sentinel-oracle/config.json', 'utf8')))"Sentinel Oracle is a community-driven project. Contributions of all kinds are welcome — code, bug reports, feature ideas, documentation improvements, and security research.
| Resource | Purpose |
|---|---|
| CONTRIBUTING.md | How to contribute, coding guidelines, PR process |
| SECURITY.md | How to report a vulnerability |
| Issues | Bug reports and feature requests |
| Pull Requests | Open PRs awaiting review |
We have no paid security team — we rely on the community to help keep this project secure. Vulnerability reports are sincerely appreciated.
Sentinel Oracle is released under the Business Source License 1.1 (see LICENSE). In plain language:
- ✅ You can use, modify, and improve Sentinel Oracle for your own development, internal operations, or maintenance — whether you're an individual developer, a startup, a bank, or a large enterprise.
- ❌ You cannot sell, sublicense, or charge for the software itself, nor incorporate it into a paid commercial product or a hosted/managed service (SaaS, PaaS, cloud hosting).
- ✅ Internal business use is always permitted. You can build internal tools, dashboards, or custom integrations on top of Sentinel Oracle without restriction, as long as they are not sold or monetized externally.
- ✅ Contributions back to the project (bug fixes, features, documentation) are welcome and encouraged under the same license terms.
If your company relies on Sentinel Oracle to protect your merge pipeline and you want to give back — contributions, security research, and documentation improvements are the best way. No payment required.
BUSSL-1.1 -- see LICENSE for terms.