Credential exposure register — 2026-08-25¶
Status: OPEN. No item here is remediated. Owner: SENTINEL to scope; the existing credential owner executes rotation.
This exists because the findings below were established across several agent sessions and messages, and a message dies with its conversation. This file is the durable record. If you are a SENTINEL session picking this up, start here.
No credential value was read to produce this. Every entry is derived from filenames, paths, git metadata, API flags, or another agent's already-recorded finding. A second read of exposed credential material is a second copy, not verification.
P0-1 — production credential set committed to main¶
| Field | Value |
|---|---|
| File | deployments/oracle/.env.bak-hq-ox1 |
| Project | blueflyio/agent-platform/infra/agent-docker |
| Branch | main (absent from release/v0.1.x) |
| Entered | 8ae8c93, 2026-07-24 — one commit, never modified |
| Duration | 32 days as at 2026-08-25 |
| Divergence from live | 1 byte |
| Scope | 92 credentials |
| ROTATION_REQUIRED | ESTABLISHED |
| HISTORY_EXPOSURE_REMEDIATED | NO |
Source of the 92-credential finding: SENTINEL, recorded in agent-docker !312's description, 2026-08-24 — "the committed .env.bak-hq-ox1 being 1 byte different from the live 92-credential production .env … meaning ubuntu-uyww's rotation scope may be far larger than currently assumed".
All 92 are in git history. No file deletion recovers them. Rotation is the only remediation. MR !314 attempted the deletion and is not a fix.
P0-2 — 29 unmasked group CI/CD variables¶
Group blueflyio, masked=false; several also protected=false, so
unprotected branches receive them. Any job in any project in the group can echo
them into a persisting log.
By key only: SA_TOKEN_OSSA_VALIDATOR, SA_TOKEN_MODULE_GENERATOR,
DEPLOY_DRUPALORG_TOKEN, DRUPALORG_PROJTOKEN_api_normalization,
GITLAB_OBSERVABILITY_TOKEN, GITLAB_REGISTRY_NPM_TOKEN, GITLAB_AGENTNAS,
and three private keys — DRUPAL_ORG_SSH_KEY, NAS_SSH_KEY,
ORACLE_SSH_KEY.
Rotation order should lead with NAS_SSH_KEY and ORACLE_SSH_KEY: they reach
the durability plane and production runtime respectively.
Known consumer, and the same defect from another angle: GITLAB_REGISTRY_NPM_TOKEN
appears in agent-docker's .npm-auth block inside a three-deep fallback chain
(${GITLAB_REGISTRY_NPM_TOKEN:-${NPM_TOKEN:-${CI_JOB_TOKEN}}}), which §14
prohibits — a job using it cannot report which identity authenticated.
P0-3 — two .env files tracked in git¶
.env in blueflyio/agent-platform/models/rfp, and .env in project
76284338. Committed, therefore in history — deletion alone is
insufficient; these need rotation plus history rewrite.
Deliberately not read. A file named exactly .env tracked in git is the
highest-probability real credential in an estate; opening it to classify it
creates the exposure the classification exists to prevent.
P0-4 — NAS file, world-writable¶
/volume1/AgentPlatform/home-mcp-env/dotenv.plaintext.quarantined.20260721-121030
— 590 B, mode 777, on durable NAS storage since 2026-07-21. Surfaced by
HARBORMASTER. Filename says quarantined; mode 0777 is not quarantine.
P0-5 — hotfixDeploy.sh runs set -x around an interactive SSH password¶
Reported by another lane. Process-level exposure: the credential reaches process listings and any captured job output.
P0-6 — unresolved: does assets/bluefly release still carry the !85 literal?¶
blueflyio/assets/bluefly (PROJECT_ID=57890741) MR !85, "security: scrub
hardcoded GitLab PAT and Google API key" — open against release/v0.1.x since
2026-08-06, conflicts=true, +5/-4 across three files.
Two thirds of it is already resolved on the release line. Measured by blob identity across branch tips, contents never read:
| path | main |
release/v0.1.x |
|
|---|---|---|---|
.gitlab/cleanup_artifact_tags.sh |
b135064f |
b135064f |
identical |
config/ (tree, contains google_fonts_api.settings.yml) |
f4324326 |
f4324326 |
identical |
.gitlab-ci.yml |
01b2d25b |
22a31a51 |
differs |
main's tip is the merge commit of !95 "security: scrub secrets"
(merged 2026-08-06), so main's versions of the first two files are the
scrubbed ones, and release/v0.1.x carries byte-identical blobs.
The one open question, for the secrets lane only: does
blueflyio/assets/bluefly at release/v0.1.x still contain, in
.gitlab-ci.yml, the literal that !85 removes?
A differing CI blob is not evidence the credential is still present — CI
config diverges for ordinary reasons. Answering it requires reading
.gitlab-ci.yml at the release tip to check for a literal, which is reading a
file to confirm a credential's presence. Deliberately not done, on the same
boundary as oracle-op-refs.map.yaml: the artefact decides, not the tool.
Disposition — REDUCE_AND_RESOLVE. If the literal is gone, !85 closes as
fully superseded. If it remains, the correct end state is a small MR touching
.gitlab-ci.yml alone, on a non-conflicting branch, targeting
release/v0.1.x; !85 cannot merge as-is regardless.
The title is itself a disclosure. !85 names both credential types and has
been publicly titled for 19 days. Closing it is a small remediation in its own
right, not housekeeping, and should be described that way.
P0-8 — 2026-09-03 — live GCP API key in config/sync/google_fonts_api.settings.yml¶
| Field | Value |
|---|---|
| Project | blueflyio/assets/bluefly.io (id 80431142) |
| File | config/sync/google_fonts_api.settings.yml, line 3, key google_api_key |
| Branch | present at release/v0.1.x HEAD (checked directly — not stale, not already scrubbed) |
| Also GitLab-detected | vulnerability 365336049, Secret Detection / Gitleaks, severity critical, state detected, first seen pipeline 2794774916 (~2026-08-27) |
| ROTATION_REQUIRED | ESTABLISHED — this is a live Drupal config file, not history-only; the key is readable by anyone with repo access at the current tip, not just via git log |
| ROTATION_VERIFIED | NO |
Distinct from P0-6 in this same file: P0-6's diff table lists config/ as a
tree whose blob is identical between main and release/v0.1.x and treats
that as already scrubbed — that comparison was about different files
(.gitlab/cleanup_artifact_tags.sh, the google_fonts_api.settings.yml
tree entry as of 2026-08-25). This key was independently detected by
GitLab's own scanner on 2026-08-27, two days later, and is confirmed present
right now. Whether it was reintroduced after an earlier scrub or was never
covered by it is not established here — only that it is live today.
A second, self-inflicted exposure happened while investigating this
finding: the file was read directly to confirm the vulnerability was
current rather than checked by hash/pattern only, printing the live key
value into an interactive agent session transcript. Recorded here as its
own instance of this file's own opening principle — "a second read of
exposed credential material is a second copy, not verification" — restated
without repeating the value: confirming a scanner finding does not require
reading the secret; a content hash, a grep -c boolean, or GitLab's own
finding metadata (already includes the location and a content hash in its
raw_metadata) is sufficient and was available.
Remediation is two actions, not one: (1) revoke/regenerate the key at GCP
(https://console.cloud.google.com/apis/credentials per GitLab's own remediation
text on the finding) — deletion from git alone does not do this, matching
this file's rule for P0-1/P0-3; (2) replace the config-committed literal
with a reference (Bluefly's key module / 1Password-backed secret,
consistent with STD-SEC-001-authentication-and-secrets.md §11's
"references, not values") so a future config-sync re-import cannot
reintroduce a literal.
Artefacts that now themselves carry exposed material¶
Recorded because they are consequences of investigating the above, and deleting them destroys evidence of what was exposed and when. None should be removed without a decision that accounts for that.
- A session transcript in which group variable values were printed during an API read.
- An MCP tool-result file on the workstation at
~/.claude/projects/…/tool-results/…get_merge_request_diffs-1787689756233.txt, containing the diff of a secret-file deletion — which necessarily contains the secret. Created while extracting paths; never read.
Ownership — established in source, and it is absent¶
The rotation-owner question for P0-1 was routed to source before the secrets store, and it fails one step earlier than expected:
blueflyio/agent-platform/infra/agent-docker declares no owner for any path
in it. No CODEOWNERS at the repository root, none at .gitlab/CODEOWNERS,
no .ownership.yml. Source-derived and safely established.
This separates two things previously treated as one — "the SENTINEL lane is unstaffed" and "no owner is declared anywhere in the repo" need different repairs, and the second is a repository defect fixable without touching the secrets store.
OWNER=NOT_ESTABLISHED. Artefacts deliberately not checked, named rather
than glossed:
- 1Password vault / item mapping
- 1Password Connect instance ownership
- Service-account issuance
deployments/oracle/oracle-op-refs.map.yaml— present in the same directory as the P0-1 exposure. Deliberately unread. A committed map ofop://references is the credential estate map in source form; reading it puts the same thing into a transcript that enumerating the vault would, through a different door. Secrets lane only.
P0-1 status re-checked: deployments/oracle/.env.bak-hq-ox1 and
deployments/oracle/compose.lock.yaml are both present at main's HEAD as
of 2026-08-25. Three MRs appeared to address this; none removed either file.
Measurement rule this register depends on¶
A count is scoped to its credential — and "no credential" is a scope.
op run --account blueflyiollc -- <cmd> with no --env-file and no op://
reference in the environment injects nothing. It exits 0, the variable is
empty, and curl --header "PRIVATE-TOKEN: " goes out unauthenticated.
Verified without printing the value: the guard [ -n "$TOKEN" ] reported
VAR_EMPTY.
Three readings taken that way, and each looked like data:
| observation | wrong reading | actual meaning |
|---|---|---|
| group open MRs = 5 | a narrow credential's view | the public MR count |
PROJECT_ID=57890741 -> 404 |
this token lacks access | GitLab's answer to an anonymous caller for a private project |
/approval_rules -> 401 (x10) |
— | the failure that finally exposed it |
This is the most dangerous scope precisely because an unauthenticated read of a partly-public group returns a plausible small integer instead of an error.
Required guard: assert the variable is non-empty before any authenticated
call — [ -z "$TOKEN" ] && exit 1. One line; it converts a silently-wrong
answer into a loud failure.
Authenticated paths are unaffected: writes and protected-branch/settings reads cannot succeed anonymously, so any call that performed one was carrying a real credential. Every count must still state which path produced it.
What is required¶
- SENTINEL scopes rotation. P0-1 is 92 credentials and leads.
- The actual credential owner executes.
ROTATION_SCOPE_OWNER=SENTINEL,ROTATION_EXECUTION_OWNER=ACTUAL_CREDENTIAL_OWNER,CREDENTIAL_OWNER=NOT_ESTABLISHEDuntil proven. REFINERY's lane is source and delivery-path convergence — the ability to inspectCODEOWNERSor repair a delivery path does not make a lane the credential authority.THOMAS_REQUIRED=NOT_ESTABLISHED— escalation is justified only if no governed owner exists, broader authority is needed, or the authority model itself must change. If the scoping work finds no owner for a credential, that absence is the finding to escalate, not the rotation. - Do not treat file removal as remediation.
FILE_REMOVEDis notCREDENTIAL_INCIDENT_CLOSED, and every item here is in history.
Model gap found alongside this¶
The governed branching topology has no path to delete a file that exists only
on main. A release→main promotion cannot delete content the release branch
does not have. Measured on P0-1: !312 reconciles main into release, but its 40
changed paths include neither secret file, and it currently has conflicts.
That is a defect in the model, it will recur for any main-only file, and it is not to be worked around with a direct-to-main MR.