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.