Skip to main content

Documentation Index

Fetch the complete documentation index at: https://www.aidonow.com/llms.txt

Use this file to discover all available pages before exploring further.

Three CI security gaps: token in URL, audit blind spot from missing Cargo.lock, shared agent credentials

Executive Summary

A security review of CI workflows across four Rust service repositories identified three independent gaps, each representing a different class of risk: credential exposure through URL construction, audit coverage loss through lockfile exclusion, and blast-radius amplification through shared credentials. None of the three required a security incident to discover — each was identifiable through static analysis of the CI workflow files and repository configuration. All three were remediated in a coordinated single-day sweep. A fourth improvement — a pre-push cargo check hook — was added in the same session as a developer-facing guard against the wasted CI cycles that delayed the security review’s discovery. This paper documents each gap, its remediation, and the latent gap that the sweep did not address.

Key Findings

  • Inline token interpolation in git clone URLs exposes credentials in multiple locations simultaneously: the token appears in git remote -v output, in process argument lists visible to other processes on the same runner node, and potentially in CI runner logs — three independent exposure vectors from a single misconfiguration.
  • Excluding Cargo.lock from version control creates an audit blind spot that is invisible at the point of configuration: cargo audit cannot detect vulnerabilities in transitive dependencies it cannot see, and the absence of a lockfile produces no warning — the audit simply produces a clean result against incomplete data.
  • Shared credentials between agent classes are non-attributable, non-scopeable, and non-rotatable at the class level: any agent using a shared credential set can take actions attributable to any other agent using that set, the blast radius of a compromised credential covers all agents simultaneously, and rotation requires all agents to receive new credentials simultaneously.
  • RUSTSEC advisories against transitive dependencies remain undetected until Cargo.lock is tracked: two advisories against rustls-webpki (TLS name-constraint bypass) were surfaced immediately upon lockfile inclusion — they had been present in the dependency graph undetected.
  • A reusable shared CI workflow can re-introduce a fixed pattern across all consuming repositories: the token-in-URL fix was applied to individual per-repository workflow files but not to the shared reusable workflow, creating a latent gap that affects any repository using that workflow.
  • Proactive security sweeps are more effective than reactive incident response for this class of credential management gap: the token-in-URL pattern had been in place across multiple repositories without producing an observable incident. Detection required intentional review, not failure signal.

1. Gap 1: Token Interpolation in Git Clone URLs

1.1 The Pattern and Its Exposure Vectors

CI workflows that clone private repositories must authenticate to the version control system. A common approach embeds the token directly in the remote URL:
# Before — token embedded in URL
REPO_URL="https://${TOKEN}@gitea.example.com/org/repo.git"
git remote add origin "$REPO_URL"
git fetch --depth=1 origin
This pattern exposes the credential in three independent locations:
  1. git remote -v — Git stores the full remote URL, including the embedded token, in .git/config. Any process with filesystem access to the runner workspace can read it.
  2. Process argument list — The URL is passed as a positional argument to git remote add. On Linux, process arguments are readable via /proc/<pid>/cmdline by any process running as the same user.
  3. CI runner logs — Workflow runners typically log command output. The URL, including the embedded token, may appear in verbose output, error messages, or debug logs depending on runner configuration.

1.2 The Fix: http.extraHeader

Git’s http.extraHeader configuration injects a custom HTTP header into all requests matching the configured URL scope, without the header appearing in the remote URL or as a command argument:
# After — token in HTTP header, not in URL
git config http.extraHeader "Authorization: token ${TOKEN}"
git remote add origin "https://gitea.example.com/org/repo.git"
git fetch --depth=1 origin
The token is set via git config, not as a URL component. git remote -v shows only the clean URL. The git config command’s argument includes the token value, but this is a transient operation rather than a persistent URL stored in .git/config.
git config http.extraHeader applies the header to all requests matching the configured scope. When using --local scope (the default), it applies only to the current repository. When using --global, it applies to all repositories for the current user. In CI environments, use --local or set the config directly without scope flags to restrict the header to the current workflow’s repository clone.

1.3 Scope and the Latent Gap

The fix was applied to four repositories’ per-file workflow configurations in a single session. A reusable shared workflow used by multiple repositories across the organization was not included in the sweep. At the time of the sweep, the shared workflow retained the inline token pattern. This is a characteristic failure mode of per-repository security fixes: the fix is correct in scope but does not propagate to shared infrastructure. Any repository subsequently added that uses the reusable workflow rather than its own inline CI inherits the unfixed pattern.
The complete fix requires applying the http.extraHeader pattern to the reusable shared workflow as well. Until that change is made, new repositories adopting the shared workflow will introduce the same vulnerability that was remediated in the per-repository configurations.

2. Gap 2: Cargo.lock Exclusion and the Audit Blind Spot

2.1 The Mechanism

cargo audit checks a Rust project’s dependencies against the RustSec Advisory Database. The audit operates against the lockfile, not the Cargo.toml manifest — the lockfile is the only artifact that contains the exact resolved version of every transitive dependency. When Cargo.lock is excluded from version control (a common practice for library crates, sometimes incorrectly applied to service crates), CI builds regenerate the lockfile on each run. The lockfile exists on the runner during the build but is never committed. When cargo audit runs in CI, it audits the locally generated lockfile — but that lockfile is ephemeral and may differ between runs if dependency resolution produces different results. More critically: the audit produces a clean result even when the lockfile is absent or incomplete. There is no warning that audit coverage is partial. The silent clean result is the risk — it communicates more confidence than is warranted.
# .gitignore — before
Cargo.lock    ← excluded; audit covers only direct deps resolved at CI time

# .gitignore — after  
# Cargo.lock removed from exclusions; lockfile committed
# audit now covers full transitive dependency graph consistently

2.2 What the Lockfile Inclusion Surfaced

Two advisories against rustls-webpki appeared immediately upon lockfile inclusion and consistent audit execution:
AdvisoryCrateVersionClass
RUSTSEC-2026-0098rustls-webpkiTLS name-constraint bypass
RUSTSEC-2026-0099rustls-webpkiTLS name-constraint bypass (variant)
Both advisories concern TLS certificate name-constraint validation for user-controlled certificate names. In the affected service’s deployment model — server-to-server internal communication using infrastructure-managed certificates, with no user-controlled certificate names — the advisory’s attack vector is not reachable. The correct response was to document the exception explicitly rather than silently ignoring it.

2.3 Declarative Exception Management

The immediate fix was to add inline --ignore flags to the cargo audit command. This was subsequently replaced with a declarative audit.toml file:
[advisories]
ignore = [
    # rustls-webpki TLS name-constraint bypass (RUSTSEC-2026-0098, RUSTSEC-2026-0099)
    # Affects TLS certificate validation for user-controlled certificate names.
    # Not reachable in server-to-server internal mTLS usage with
    # infrastructure-managed certificates.
    # Review by: 2026-07-18 — upgrade when a patched version is available.
    "RUSTSEC-2026-0098",
    "RUSTSEC-2026-0099",
]
The declarative approach has three advantages over inline flags: the rationale is documented in the repository rather than in a workflow file comment; the exception survives workflow refactors without needing to be re-added; and the review-by date creates an explicit future obligation rather than an indefinite suppression.
Every cargo audit exception should carry three pieces of information: the advisory identifier, the rationale for why the advisory’s attack vector is not reachable in this specific deployment, and a review-by date after which the exception should be reconsidered. Without a review date, audit exceptions accumulate indefinitely and lose their meaning.
A third advisory — RUSTSEC-2026-0037 against quinn-proto < 0.11.14 — was also surfaced and was actionable: a patch was available. The dependency was bumped to resolve it.

3. Gap 3: Shared Agent Credentials

3.1 The Problem

An AI agent execution system where multiple named agent classes (development agent, security agent, architecture agent, operations agent) operate against a shared project management platform presents a credential management question: does each agent class authenticate with its own identity, or do all agents share a single service account? The initial implementation used a single shared bot account. All task comments, status updates, and API calls were attributed to that account regardless of which agent class was executing. This created three compounding problems: Non-attributability. Audit logs showed actions taken by a single account, making it impossible to determine which agent class had performed which action. Debugging incorrect agent behavior required cross-referencing timestamps with session logs rather than reading audit attribution directly. Non-scoped blast radius. A compromised credential gave access to the full scope of permissions held by the shared account — not the subset appropriate to any particular agent class. An agent class that should only be able to update task status could, through the shared credential, perform any action the account was authorized for. Non-class-level rotation. Rotating credentials required all agent classes to receive new credentials simultaneously. There was no mechanism to rotate a single agent class’s credentials without affecting all others.

3.2 Per-Agent Vault Injection

The fix introduced per-agent credential injection via HashiCorp Vault AppRole authentication. A mapping table in the agent daemon resolves a named agent class to a Vault secret path at invocation time:
_ASSIGNEE_TO_VAULT_PATH: dict[str, str] = {
    "dev-agent":      "secret/data/agents/dev-agent",
    "security-agent": "secret/data/agents/security-agent",
    "arch-agent":     "secret/data/agents/arch-agent",
    # ... additional agent classes
}
At invocation, the daemon performs an AppRole login against Vault, reads the credential for the specific agent class, and injects it into the session environment. The login is per-invocation; credentials are not cached between sessions. The fallback is explicit and non-fatal: if Vault is unreachable, the daemon falls back to a default credential rather than failing the invocation. This is intentional — credential injection failure should degrade gracefully rather than halt the development workflow.

Credential Model Comparison

PropertyShared AccountPer-Agent Vault Injection
AttributionSingle identity in audit logsPer-agent-class identity
Blast radiusFull account scopeScoped to agent class permissions
RotationAll agents simultaneouslyPer-agent-class, independent
Failure modeN/AGraceful fallback to default
Vault dependencyNoneRequires Vault availability

4. Gap 4 (Prevention): Pre-Push Cargo Check

The security sweep was preceded by a period of CI-driven build feedback on compile errors — developers discovering type errors and missing dependencies only after pushing and waiting for the CI pipeline. A pre-push hook running cargo check --release --workspace was added alongside the security fixes:
#!/usr/bin/env bash
set -e
echo "→ Running cargo check --release --workspace..."
cargo check --release --workspace 2>&1
The CI-side equivalent adds cargo check as the first job in the pipeline, gating all subsequent jobs — formatting, linting, security audit — on a successful compile check. This produces compile error feedback in approximately two minutes rather than after the full pipeline completes. This is not a security control — it is a developer experience improvement that reduces the cost of iteration during security remediation work and general development alike.

5. The Latent Gap

The token-in-URL fix was applied to four per-repository CI workflow files. The shared reusable workflow — used by repositories that delegate to it rather than maintaining their own CI — was not updated. This gap is latent: it does not currently affect the four repositories where the fix was applied, but it does affect any repository that uses the shared workflow, and any new repository that adopts it in the future. Applying the http.extraHeader pattern to the shared workflow requires a single change, but it must be coordinated: the shared workflow change must be compatible with all repositories that consume it. This is the organizational cost of shared infrastructure — fixes require broader coordination than per-repository changes.

6. Recommendations

  1. Audit all CI workflow files for inline token interpolation in URLs before the first credential rotation. Token rotation reveals token exposure: the old token stops working at the moment it is rotated, surfacing every location it was embedded. Finding those locations before rotation is substantially less disruptive than discovering them during a rotation event.
  2. Commit Cargo.lock for service crates, not just library crates. The Rust ecosystem convention of gitignoring Cargo.lock for library crates is correct — libraries should not constrain their consumers’ dependency resolution. Service crates, which have no downstream consumers, should commit the lockfile to enable reproducible builds and consistent audit coverage.
  3. Replace inline --ignore flags in cargo audit with a declarative audit.toml file. Inline flags are lost on workflow refactors and carry no documented rationale. Declarative exceptions with rationale and review dates create an auditable suppression record.
  4. Assign each agent class a distinct credential set scoped to its required permissions. The blast radius of a compromised or misbehaving agent should be bounded by what that agent class is authorized to do, not by the full scope of a shared service account.
  5. Apply all security fixes to shared reusable workflows, not only to per-repository configurations. A fix that does not propagate to shared infrastructure is a partial fix. Identify all consumers of the shared workflow before marking the finding resolved.
  6. Add cargo check as the first CI job, gating all subsequent jobs. Compile errors discovered after security checks, lint checks, or test runs waste pipeline resources and delay feedback. A two-minute compile gate at the start of the pipeline is the highest-leverage single improvement to CI feedback latency.

Conclusion

The three gaps documented here share a common characteristic: none produced an observable failure before they were found. The token had not been leaked; the audit had not missed a critical CVE that caused an incident; the shared credentials had not been abused. The gaps were structural, not symptomatic. This is the defining characteristic of this class of security gap: it is invisible until it matters. A token embedded in a URL produces no warning. A missing lockfile produces no audit error. A shared credential produces no alert when it is used by the wrong agent. Proactive review of CI configuration, dependency management practices, and credential architecture is the only reliable detection mechanism. The remediation effort — a single coordinated session across four repositories — took less time than a typical incident response. The asymmetry between prevention cost and remediation-after-incident cost is the strongest argument for scheduled security sweeps of CI infrastructure.
Code examples are sanitized and generalized. No proprietary information is shared. Opinions are my own and do not reflect my employer’s views.