Skip to content

October 2026 access review

Date: 2026-10-10. Reviewer: assigned Security worker. Scope: repository inspection only, revalidated against the frozen task v14. This report describes access the source permits and the custody it documents. Live users, grant instances, expiry dates, container state, firewall rules and deployed overlays were not queried. Prior release reports are historical evidence, not proof of today's production configuration. No credential was rotated or access changed.

The register names the credentials and proposed intervals. All findings below are open recommendations. No repair task has been created by this lease-bound worker; the coordinator should create one per finding and attach its ID and closure evidence. The owner must approve every production, provider-permission, secret or custody change. Security can prepare and review those tasks, not enact the grants or deployments.

Access paths into production

Path / component and source What it can do / current boundary Assessment
Dedicated SSH channel → runtime OS user heyaira; docs/DEPLOYMENT.md, scripts/production_catalog_background_canary.py Deploy and run docker exec in API/backup/PostgreSQL; admin CLI can issue/revoke credentials, edit memberships and bind repositories; canary seeds synthetic DB rows. No actual SSH login roster in source Necessary DevOps authority, too broad for recurring restore automation (R04); verify authorized keys/sudo monthly (R10)
Tailscale private network; docs/BRIDGE_PROTOCOL.md Device/node identity and ACLs grant network reachability; Bridge API still requires its own credential Actual node roster and ACLs unverified; include in owner-confirmed least-access review (R10)
Separate root-admin/sudo → Caddy/host; ops/caddy-heyaira.sh, docs/DEPLOYMENT.md Edge configuration and full host recovery; deliberately separate from runtime account Keep break-glass and edge changes separate, owner-authorized (R10, R11)
Runtime user / rootless Docker → container exec and secret mounts; compose.yaml Reads application secrets and DB data; rootless limits host authority but does not isolate secrets from the runtime user or API process API receives more writable secret files than it needs (R01)
Shared DB login / DB owner; compose.yaml, src/heyaira/config.py, src/heyaira/db.py API, Beat, runner and backup share one login; startup migrations need DDL. Bootstrap role is PostgreSQL-image administrative role; no public DB port Wider than each runtime needs (R02); deployed role attributes unverified
heyaira-ci-a / heyaira-ci-b, common heyaira-ci user/label; .github/workflows/ci.yml Pull requests execute Python, shell and Docker services on persistent self-hosted infrastructure; workflow token is contents-read; both share Docker daemon Indirect source/CI trust path, no production deployment credential evidenced; cross-job isolation gap (R05). Individual runner names supplied by task
GitHub App → API and /github/webhooks; docs/GIT_DOCUMENT_ACCESS.md, docs/DEPLOYMENT.md Short-lived installation tokens read bound repositories/PR/CI; HMAC webhook supplies CI results. GitHub is an input to publication acceptance Separate read/check App from Bridge publishing; verify selected repositories and no unnecessary write permissions (R10)
Bridge Mac → bridge:transport; docs/concepts/bridge-enrollment.md, docs/concepts/bridge-project-scope.md Bound-project task/memory feeds, claim/renew/report execution, publication metadata; proof of identity and explicit project bindings Broad across bound projects by design; review bindings/account slots; do not replace with owner token (R07, R10)
Paired-node access/refresh → /bridge/v1/heartbeat; docs/BRIDGE_PROTOCOL.md Separate credential keeps paired node connected; single-project reads, access 15 minutes, fixed refresh 30 days Still required, not retired; fixed refresh needs explicit lifecycle review (R08)
Per-execution MCP / claim lease; docs/concepts/task-version-execution.md Worker reads exact task and memory, appends RESULT/receipt; verifier reads only; lifetime tied to lease Appropriate narrow task boundary. Interactive task claim secret is separate and not a general project bearer
Public OAuth routes, static project tokens → MCP; docs/MCP_PUBLIC.md, src/heyaira/oauth.py Consent requires static project credential; token project/resource binding, current-role intersection, refresh replay protection and revocation Current grants unknown; avoid standing owner tokens/default broad scopes in ordinary clients (R08)
iOS public client a7c2be96 (public identifier); ios/HeyAiraBridge/HeyAiraBridge/Info.plist, OAuthClient.swift PKCE with eu.heyaira.bridge://oauth/callback; memory:read tasks:read documents:read notifications:write offline_access; Keychain access/refresh; shared /mcp Current tabs justify read scopes. No task write/admin scope requested; legacy notification-write exception and shared resource need retirement plan (R08)
Owner WebAuthn → /decide/, /decide/enroll, /decide/keys; docs/specs/decisions/D1b-implementation.md Passkey user verification answers/authorizes decisions; account enrollment code comes only by owner-approved e-mail; HMAC link alone cannot answer Distinct from OAuth and bearer authority; review enrolled devices and protect mail/recovery custody (R10)
API → OpenBao device vault; ops/openbao-production-compose.yaml, ops/openbao-mobile-policy.hcl Scoped application token reads/writes/deletes mobile device tokens; root/unseal offline administration can access broader vault Scoped prefix is good; shared-network HTTP and manual root/unseal custody need confirmation/hardening (R06, R10)
APNs provider key and Apple developer account; docs/BRIDGE_IOS_COMPANION.md Send push to registered devices; signing/release account separately controls mobile releases Not MCP data access; restrict topic/team and release operators, review stale devices (R10)
Backup/retention R2 keys and private age identity; docs/BACKUP_RUNBOOK.md Writer uploads but provider scope also deletes; retention deliberately deletes; only isolated restore Mac can decrypt Writer compromise can destroy recovery copies (R03); current bucket locks unknown
Runner/watchdog → repair broker Unix socket; ops/server-beat-repair-compose.yaml, ops/systemd/, docs/SERVER_BEAT_RUNBOOK.md Broker alone holds Docker socket; allowlisted same-project app/Beat restarts; no PostgreSQL/OpenBao restart or automatic unseal Preserve narrow command boundary; actual socket ACL/allowlist needs monthly attestation (R10)
External Cloudflare Worker → pulse, SES and OVH reboot; ops/cloudflare/server-beat-watchdog/src/index.js Pulse bearer reports liveness; SES API sends alerts; OVH grant plus independent health probes/budgets can reboot approved VPS Recovery/control-plane authority; pulse and reboot grants need independent restricted custody (R10); no DB/project content in Worker
Owner/provider dashboards → Cloudflare DNS/TLS/Pages/Worker/R2, OVH, AWS, GitHub, Apple; docs/WWW_SETUP.md, docs/operations/documentation-workflow.md, runbooks Infrastructure, source publication, secret issuance and backup control; Git-connected Pages publishes main High-impact human control plane; exact membership/credential scopes unknown (R10, R11)
GTM/GA4/Search Console editors; docs/analytics/README.md, docs/analytics/SEARCH_CONSOLE.md Marketing-site tags, analytics and site verification; not MCP authorization Separate website publishing authority; monthly editor review (R10)

OAuth scope reference (src/heyaira/oauth.py): defaults are memory:read, memory:write, tasks:read, tasks:write, documents:read, agents:read, offline_access. Explicit additional scopes are bridge:bindings:write, agents:write, notifications:write, org:write. Static tokens retain memory:read, memory:write, tasks:read, tasks:write, agents:read, bridge:bindings:write, notifications:write, intersected with current role; they do not automatically gain document/catalog/organization write scopes. Owner/admin membership is distinct from an OAuth scope and from a DB role.

P1 — no confirmed incident in this review

No real-looking credential value was identified by the bounded source scan and inspection. No production compromise, exposure or outage is asserted. This does not certify Git history, ignored files or live installations as secret-free. A discovered candidate must be recorded only as possible secret committed at <path>:<line>, without any value or fragment, and escalated for owner-approved containment. No fictitious P1 is created to fill a severity category.

P2 findings — reachable excessive authority

R01 — API mounts the whole secret directory writable

Source: compose.yaml server volume ./secrets:/run/heyaira-secrets; docs/DEPLOYMENT.md keeps App key, webhook secret, static token and recovery output in this directory; ops/server-beat-repair-compose.yaml also documents pulse and SES files there. Cause/consequence: an API-process compromise can read or overwrite every file present in that mount, including credentials whose consumer is another component; rootless Docker does not remove this application-level authority. Which optional files are present is unverified.

Narrowing: mount only the API's required key/token files read-only; give admin CLI token-generation/recovery operations a separate temporary writable output directory and keep pulse/backup/custody material outside the API mount. Implementation owner: DevOps with backend maintainer. Required agreement: owner for secret placement and production rollout. Closure evidence: sanitized mount-name/mode inventory plus API and admin-output regression checks, with no file contents.

R02 — all database services share bootstrap/migration authority

Source: compose.yaml maps the same HEYAIRA_DB_USER and password to PostgreSQL initialization and four application services; src/heyaira/db.py applies migrations at startup. Cause/consequence: fresh PostgreSQL-image bootstrap gives the initial user administrative authority; even if production has reduced it, the source still couples all runtimes to the migration login. A scheduler or backup compromise reaches unrelated application tables and DDL. This is a source-topology finding, not a claim to have read production grants.

Narrowing: separate a non-runtime migration owner, API DML role, bounded Beat/runner queue roles and backup read/queue role. First map required tables, sequences and functions, then prohibit superuser/role creation/schema DDL for normal runtime logins. Implementation owner: backend/DB maintainer and DevOps. Required agreement: owner for roles, credential rotation, startup changes and deployment. Closure evidence: privilege tests against isolated PostgreSQL plus owner-authorized, value-free production role attestation.

R03 — backup writer can delete the recovery set

Source: docs/BACKUP_RUNBOOK.md, “R2 credentials: one key writes, another deletes”; compose.yaml retention reads backup.env and falls back to its key when retention.env is absent. Cause/consequence: R2 Object Read & Write also permits deletion, so key separation alone does not protect backups from the always-running writer. Actual bucket locks are unknown, and missing retention credentials silently preserve shared authority with a log warning.

Narrowing: owner-approved bucket locks/immutable recovery copy with a retention-compatible duration; require the dedicated retention key instead of fallback; use a read-only restore credential, with a separate bounded status report path if needed. Do not promise a write-without-delete R2 scope that the provider does not offer. Implementation owner: DevOps/backup maintainer. Required agreement: owner for provider policy, costs, retention/deletion and keys. Closure evidence: policy attestation and isolated recovery and retention validation proving the protected copy remains recoverable.

R04 — scheduled restore accepts arbitrary runtime-user shell

Source: ops/restore-rehearsal/README.md requires passwordless sudo -u heyaira; ops/restore-rehearsal/mac-restore-rehearsal.sh sends client-supplied sh -s to that user; server-helper.sh runs docker exec. Cause/consequence: the restore Mac has authority to submit arbitrary shell as the production runtime user, wider than downloading encrypted objects and reporting one result. This does not assert the Mac or SSH key is compromised.

Narrowing: dedicated restore SSH identity with a forced, server-owned helper accepting only manifest, validated object, and report; disallow shell, PTY and forwarding; remove arbitrary sudo. Prefer direct read-only R2 download plus bounded report submission where practical. Keep private age identity off the server. Implementation owner: DevOps/restore maintainer. Required agreement: owner for SSH/sudo and credential changes. Closure evidence: negative shell/forwarding/argument tests and a successful isolated restore, followed by an explicitly authorized production permission check.

R05 — persistent CI jobs share a user and Docker daemon

Source: .github/workflows/ci.yml runs pull_request code on self-hosted heyaira-ci; comments document two runners on one rootless Docker daemon. It installs and executes PR-controlled code. Cause/consequence: a job can interfere with the other job's containers or leave host state for later jobs; a fresh Python venv does not isolate the daemon or OS user. Repository/fork admission settings and host network reachability are unknown. No evidence shows that this runner holds production credentials.

Narrowing: ephemeral per-job runner/VM and Docker daemon, separate untrusted PR trust domain, no production SSH/provider credentials or production network route, and persist-credentials: false for checkout when further Git auth is unneeded. Pin third-party Actions to reviewed commits in the follow-up. Implementation owner: CI/DevOps. Required agreement: owner for runner, GitHub settings and any cost changes. Closure evidence: two-job isolation checks, clean teardown and value-free credential/network-boundary attestation.

P3 findings — hardening and facts requiring confirmation

R06 — vault transport is HTTP on the common Compose network

Source: ops/openbao.hcl disables TLS; the production overlay points the API to HTTP OpenBao on default, with no CA bundle. Vault is not host-published. The private network is a boundary, but not authenticated encrypted transport. Narrowing: dedicated API/vault network plus TLS and a verified CA, or a documented equivalent isolated transport; confirm actual token TTL, prefix-only policy, audit and offline unseal custody. Do not auto-unseal as a repair. docs/BRIDGE_OPENBAO.md illustrates initialization with one share and a threshold of one. Confirm whether production follows that example; ask the owner to choose either a recovery quorum with separately held shares or an explicitly documented single-custodian exception and recovery procedure. Do not infer the deployed threshold from the example or rekey without approval. Implementation owner: DevOps. Required agreement: owner for topology, vault policy and keys. Closure evidence: negative cross-service connectivity and certificate validation tests and non-secret custody/policy confirmation.

R07 — key defaults couple unrelated functions and allow weak Bridge keys

Source: src/heyaira/config.py defaults the document-cursor secret to the credential-bearing DB URL; base compose.yaml does not wire an independent cursor key. docs/DEPLOYMENT.md documents that Bridge enrollment/transport keys are not checked for length or distinctness. Narrowing: separately generated cursor key, independently generated Bridge keys with minimum entropy and distinctness validation; plan outstanding-cursor and enrollment/rotation compatibility first. No deployed weak key is asserted. Implementation owner: backend/DevOps. Required agreement: owner for key provisioning/rotation and rollout. Closure evidence: synthetic config refusal tests, value-free key generation/custody attestation and recovery verification.

R08 — legacy grants and non-expiring client secrets need an exit plan

Source: src/heyaira/oauth.py, docs/reference/mcp-endpoint.md, docs/BRIDGE_IOS_COMPANION.md, docs/BRIDGE_PROTOCOL.md, docs/reports/2026-10-03-oauth-client-secrets-candidate.md. Shared /mcp still issues grants; static tokens can confer owner/admin authority; confidential client secrets have no automatic expiry; legacy paired-node refresh does not rotate for its 30-day life. Verified iOS callback grants can perform notification writes without explicit notifications:write, subject to membership checks. These are documented compatibility behaviors, not a demonstrated tenant escape.

Narrowing: roster each active client/grant by consumer, redirect, project, role, scopes and expiry without token values; use read-only/explicit scopes where sufficient, per-consumer static principals, 90-day client-secret rotation or re-registration, and planned removal of dormant grant families. Migrate Mac and iOS to project resources before retiring shared issuance; re-consent older iOS grants before removing the exception; plan pairing refresh renewal without breaking heartbeat. Implementation owner: auth/client maintainers and DevOps. Required agreement: owner for revocation, scope reduction and production compatibility changes. Closure evidence: role/scope and expiry tests plus owner-approved grant-roster confirmation and reconnect checks.

R09 — role reference and security publication boundary are not synchronized

Source: assigned role points to docs/specs/org-v1/K0-roles.md#security, but that section is absent in this checkout. mkdocs.yml excludes analytics and specs but not this new directory. Narrowing: coordinator should reconcile the canonical role in a separate documentation task, and record whether this directory remains public-safe or should be excluded before adding private operator evidence. This delivery contains repository-derived paths and role names only. Implementation owner: coordinator/docs maintainer. Required agreement: owner for canonical role/publication policy; coordinator for the follow-up scope. Closure evidence: resolved role reference and reviewed site-publication policy; never publish live credential inventories.

R10 — custody, expiry and live allowlists cannot be attested from Git

Source: docs/BACKUP_RUNBOOK.md explicitly requires separate live custody records; docs/ARCHITECTURE.md assigns provider/publishing credentials to the Mac; docs/concepts/credential-broker.md, docs/concepts/bridge-project-scope.md, provider runbooks and workflows describe boundaries without active account rosters. Narrowing: owner/DevOps confirms last rotation/next due and holder for every register row, SSH/sudo users, Tailscale nodes/ACLs, DB roles, CI runner separation, GitHub App selected repositories, Apple/AWS/Cloudflare/OVH/Google delegates, vault recovery and provider slots. Security compares Bridge allowed_models, allowed_accounts, repository/workspace mappings and node-project bindings with approved active work, then proposes removal of unused entries. Do not infer actual installed policy from the canary's synthetic example.

Implementation owner: Security coordinates, DevOps/provider owners attest. Required agreement: owner before live collection beyond this task, and before any credential, grant or allowlist change. Closure evidence: protected, value-free attestation references with dates (first confirmation proposed by 2026-10-17), coordinator repair tasks and next review on 2026-11-10. No active credential is called expired solely because its date is unknown.

R11 — origin TLS enforcement remains unconfirmed

Source: docs/DEPLOYMENT.md says Cloudflare uses Full and instructs a later change to Full (strict) after origin certificate issuance. That wording does not prove current edge settings. Narrowing: owner-authorized DevOps confirmation of strict origin certificate validation and private origin port exposure; if still permissive, enable strict validation after checking origin certificate/renewal. Implementation owner: edge DevOps. Required agreement: owner for Cloudflare/Caddy/firewall changes. Closure evidence: value-free configuration attestation and approved TLS/renewal check, without credential or certificate private-key material.

Single owner handoff

The repository review found five concrete excessive-authority paths (R01–R05) and six hardening/confirmation items (R06–R11); no confirmed P1 incident. Prioritize API mount separation, database role separation, backup protection, bounded restore access and CI isolation. The coordinator can scope the repair tasks from the findings. Owner decisions are needed for all production, credential, provider and custody changes; none was executed here. Next monthly review: 2026-11-10. Delivery checks and remaining verification limits are in evidence.