Skip to content

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 of op:// 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

  1. SENTINEL scopes rotation. P0-1 is 92 credentials and leads.
  2. The actual credential owner executes. ROTATION_SCOPE_OWNER=SENTINEL, ROTATION_EXECUTION_OWNER=ACTUAL_CREDENTIAL_OWNER, CREDENTIAL_OWNER=NOT_ESTABLISHED until proven. REFINERY's lane is source and delivery-path convergence — the ability to inspect CODEOWNERS or 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.
  3. Do not treat file removal as remediation. FILE_REMOVED is not CREDENTIAL_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.