Close the /admin/features Firestore SSH side-door: require_service_key_or_admin + distinct agent credential #64
Open
opened 2026-08-24 23:41:26 -04:00 by logan
·
3 comments
No Branch/Tag Specified
Labels
Clear labels
area/ai-pipeline
area/auth
area/billing
area/correlation
area/discord
area/frontend
area/governance
area/growth
area/infra-deploy
area/legal
area/map
area/mqtt
area/node-ota
area/osint
area/recording-audio
area/sdr-radio
area/transcription
board
board:agenda
minutes:draft
minutes:final
role:ceo
role:cfo
role:cio
role:ciso
role:cmo
role:coo
role:cto
Scene/entity extraction, summarizer, AI providers, embeddings
AuthN/Z, roles, tenancy/org scoping, API keys, credentials
Plans, entitlements, AI spend accounting and caps
Incident correlation, linking, lifecycle, severity, corr_debug
Discord bot, radio bots, voice streams, bot tokens, webhooks
Web UI: Next.js app and the edge-node local dashboard
Board process, minutes, unattended runner, agent tooling
Go-to-market, outreach, cold email, discovery conversations
CI/CD, VM, registry, Firestore rules/indexes deploy, Terraform/Caddy
ToS, retention, licences/DAAs, CAN-SPAM, E&O, name suppression
Live map: pins, paths, tiles, legend, geocoding
Node-C2 MQTT, checkins, commands, node sweeper, dynsec
Node install, enrollment, OTA updater, image skew
External feeds: cameras, 511, ADS-B, AIS, outages, and their licences
Call capture, boundaries, PulseAudio, ring buffer, trimming, attribution
OP25 decode, SDR hardware/tuning, conventional, second SDR, mesh radio
STT, transcript correction, place verification, name accuracy
Board process: minutes and board-filed items
Needs board scoping at the next sitting; CEO removes the label once scoped
One exec pre-read, closed by CEO
CEO ruling; open until enacted
Filed by the CEO
Filed by the CFO
Filed by the CIO
Filed by the CISO
Filed by the CMO
Filed by the COO
Filed by the CTO
kind
behavior-bug
Wrong product behavior; the instrument reads it correctly
kind
measurement-bug
Corrupts the recording/instrument (debug data, evals, health checks), not behavior
priority
p0
Drop everything: prod broken, live exposure, or owner-ruled P0
priority
p1
Next up: blocks a gate, a customer, or core incident quality
priority
p2
Normal: real defect or planned work, schedule in turn
priority
p3
Low: minor, cosmetic, or someday
status
blocked
Waiting on an external party, spend, or another issue
status
in-progress
Branch or PR open against it
status
needs-decision
Waiting on an owner or board ruling before work
status
needs-info
Root cause or repro unknown; needs a reading first
type
bug
Something built is wrong: code, data, or a measurement
type
chore
Maintenance, refactor, tests, pinning, cleanup: no user-visible change
type
decision
A ruling to make or record: board minutes, owner calls, legal reviews
type
docs
Docs, runbooks, CLAUDE.md, skills, SOPs
type
feature
New capability or an extension of an existing one
type
security
Security, privacy, credential or tenant-isolation exposure
type
tracking
Tracker, checklist, initiative or punch-list spanning several changes
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: logan/server-26#64
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Decision 2, board minutes #62 (2026-08-24). Owner: CTO. Due 2026-08-31. CTO draft #60 finding 1 and CISO draft #61 finding 3 reached this independently, from different directions.
GETandPUT /admin/features(drb-c2-core/app/routers/admin.py:27-41) userequire_admin_token, which accepts only a Firebase admin token. There is no service-key path, so the unattended runbook SSHes into thec2-corecontainer and writesconfig/ai_featuresstraight to Firestore with the admin SDK (.claude/scheduled/drb-worksession.md:117-130) to stay headless. That side-door needs a full container shell to flip a global AI-cost switch, andfeature_flags.set_flags()writes noaudit_logentry either way.Work
require_service_key_or_admin(drb-c2-core/app/internal/auth.py:201-219) - already the established bot-vs-admin pattern, already wired onrouters/nodes.py:95,157,243,261androuters/trips.py:298-463. Two lines.SERVICE_KEY- that collapses two principals into one unattributable identity in every log line.audit_logwrite infeature_flags.set_flags()so a flag flip is attributable at all.config/ai_featuresand both systemsai_flags(CTO #60 rec. 2). OFF already cannot half-apply; ON can, and one missed manual step leaves a radio system hot after shutoff.drb-worksession.mdsections 5b and 5d; call the HTTPS route instead.ssh drbfrom the AI-flag path in the runbook (#62 Decision 1c).Refs #62, #60, #61.
Items 1, 3 and 4 implemented and pushed —
865b5b4, unattended run 2026-08-30, one day inside the 2026-08-31 due date. This issue stays OPEN: items 2, 5 and 6 are not finished.Tests 273 → 289 (16 new). The suite is green.
What landed
agent_service_key(envAGENT_SERVICE_KEY), separate from the Discord bot'sservice_key. New dependencyrequire_agent_key_or_adminaccepts the agent key or a Firebase admin, and rejects the Discord key — sharing one key was the failure this issue names, and the bot has no business flipping AI flags regardless. Both/admin/featuresroutes moved to it; every other route inadmin.pyis untouched.settings.agent_service_key and ...guard is load-bearing rather than stylistic:secrets.compare_digest("", "")returns True, so anycompare_digest(token, settings.agent_service_key or "")shape would turn a deployment that never configured the key into one that accepts an empty credential. Tested explicitly.feature_flags.set_flags()writes anaudit_logentry carrying before/after values and the actor, with the agent principal distinguishable from a human admin. The audit write is wrapped so its failure can neither lose the flag write nor 500 the route.config/ai_featuresdoc and every system document that actually carries anai_flagsmap. It scans for the override rather than hardcoding the two known system IDs, so a newly added system cannot silently defeat it.The cascade default is
False, deliberatelyA human-facing editor for per-system flags does exist —
PUT /systems/{id}/ai-flags(routers/systems.py:108) and theAiFlagsPaneltoggle in the frontend systems page. A per-system override is therefore visible operator intent, and cascading by default would silently erase it on any unrelated global flip. The runbook opts in with?cascade=true, which is where the "one missed manual step leaves a radio system hot" hazard actually lives.Item 2 — NEEDS OWNER, ~5 minutes
The code path exists; the credential value does not. Until it is set,
AGENT_SERVICE_KEYis unset and the route accepts admins only — safe, and unchanged from today's behaviour.openssl rand -hex 32infra/ansible/vault.ymlasvault_agent_service_key— a new value, not a copy ofvault_service_key, or this issue's whole point is lost/opt/drb/drb-c2-core/.envand restartc2-coreEditing that
.envby hand works but is overwritten on the next ansible run. Noenvironment:block was added to compose on purpose:c2-coreisenv_file-only, and a composeenvironment:entry outranksenv_file, so adding one would blank out a correctly-pasted value.Items 5 and 6 — deliberately NOT done, and
drb-worksession.mdis untouchedRetiring the SSH and Firestore-direct write from the runbook is blocked on item 2. The new credential does not exist in production, so the SSH path is still the only one that works; swapping the runbook to HTTPS tonight would have broken the next unattended run's ability to close an AI window — a strictly worse failure than the side-door this issue is closing. They unblock the moment the owner completes item 2, and are the whole of what remains here.
Status correction, verified in source and against production, unattended run 2026-08-31 (this issue's due date).
This issue reads as though none of it has shipped. Items 1-4 shipped a day ago in
865b5b4("Close the /admin/features side-door that needed a container shell to flip AI spend"), which is an ancestor of the live/healthSHA29c2fb1and is therefore in production:GET /admin/features(routers/admin.py:27-28) andPUT /admin/features(:43-53) both now depend onrequire_agent_key_or_admin.require_agent_key_or_admin(internal/auth.py:233) checkssettings.agent_service_key, not the Discord bot'sSERVICE_KEY. Item 2's "do not collapse two principals" requirement is honoured in the code.feature_flags.set_flags()writes anaudit_logentry (internal/feature_flags.py:116-164).But item 5 must not be enacted, and here is why.
settings.agent_service_keyisOptional[str] = None(config.py:157), and the agent-key branch is guarded bysettings.agent_service_key and secrets.compare_digest(...)(auth.py:259) - deliberately, so that an unset key cannot be matched by an empty token.Checked against production tonight:
AGENT_SERVICE_KEYis not set. It is absent from/opt/drb/.env, and inside the runningc2-corecontainersettings.agent_service_keyresolves falsy. (Value not printed; only presence was tested.)So in production the agent-key branch is dead code. The only way through
/admin/featurestoday is a Firebase admin ID token, which an unattended run cannot mint. The SSH/Firestore-direct write is therefore still the only working path for an unattended run to turn the AI window off.Consequence: retiring the SSH path from
drb-worksession.mdsections 5b/5d right now would leave the unattended run with no way to close an AI window. An open window bills every minute until something closes it. That is the single worst outcome available in this runbook, and it would have been introduced by "finishing" this issue on schedule.This is the general failure mode worth naming: the code half of a credential-dependent change shipped, the provisioning half did not, and nothing failed loudly - because the fallback still works.
DEFERRED.mdalready records that CI deploys code only (git pull,compose pull,up -d) and never writes the server's.env, so no deployment step was ever going to set this.What actually remains on this issue
AGENT_SERVICE_KEYin/opt/drb/.envand restartc2-core. Not a code change; CI cannot do it. Until this is done, items 1-4 are shipped but inert.drb-worksession.md5b/5d, and prove the HTTP path can turn the window off before the SSH path is removed, not after.config/ai_featuresand both systems'ai_flags) should be re-verified against/opt/drb/aiwin/separately - it is independent of the credential and may already be satisfied.Ordering is the whole point here: the credential lands first, the runbook changes last. Reversing that order is what turns this issue from a security improvement into an outage that bills by the minute.
Board ruling — FINAL MINUTES #97, 2026-09-01.
865b5b4, ancestor of the liveb722223). Independently verified in source by COO #90, CTO #94 and CISO #93. The board does not re-litigate these again (#97 D2).drb-worksession.md5a/5b/5d tonight is overruled (#97 D1).AGENT_SERVICE_KEYwas confirmed unset in production by two officers independently, so the agent-key branch is dead code and the SSH path is still the only working way for an unattended run to close an AI window.New dates. Item 2 by 2026-09-02, as task 2 of a single batched ~30-minute owner credential session (#97 D4, alongside #67 and the node-002 stopgap). Items 5/6 within 24h of item 2 being confirmed live, done in one sitting rather than split across runs — CTO.
The security-posture question this issue rests on (agent key over one audited HTTPS route vs. a container shell with Firestore admin credentials) is a unanimous board finding but the ruling is held for the owner — #97 H1. It does not block anything, because the work is held on provisioning either way.