DEV Community

SvenNilsson228
SvenNilsson228

Posted on

Monorepo API Credentials by Project — Scoped Key Rotation and Auditability in 2026

Short answer: group credentials by project, give each key a narrow scope, and record ownership changes as append-only audit events; rotate on a schedule and on every ownership change. This makes a key revocation explainable without tying access to one developer's account.

The useful unit is a project, not a person. A developer may leave, change teams, or work across several repositories, while a project still needs a stable service identity.

Keep the blast radius small.

How should a monorepo group API credentials by project for ownership and rotation?

Start with a project registry. Each entry has a durable project ID, repository paths, environments, owners, allowed operations, and a key policy. The key record stores only a hash or provider-side identifier; the secret value belongs in a secret manager and is injected at runtime. A pull request can change the owner list, but it cannot print the secret.

The monorepo makes this harder than it first appears. One checkout may contain search, billing, and docs services, and a shared CI job can accidentally inherit every environment variable. Map paths to projects explicitly, then let the deployment identity request one project scope. Do not infer scope from the branch name alone. Branches are easy to rename; audit records need stable IDs.

I keep three identities separate: the human who requested a key, the workload that uses it, and the approver who authorized it. That split is the difference between “Alice used the API” and “the search-indexer workload called the API after Alice approved a rotation.” The latter survives staff changes and gives an incident responder something precise to revoke.

A small policy object makes the contract testable:

from dataclasses import dataclass

@dataclass(frozen=True)
class CredentialPolicy:
    project_id: str
    environment: str
    scopes: tuple[str, ...]
    owner_team: str
    rotation_days: int


def can_issue(policy: CredentialPolicy, requested_scopes: set[str]) -> bool:
    return requested_scopes.issubset(set(policy.scopes))


policy = CredentialPolicy(
    project_id="search",
    environment="production",
    scopes=("documents:read", "indexes:write"),
    owner_team="search-platform",
    rotation_days=30,
)

assert can_issue(policy, {"documents:read"})
assert not can_issue(policy, {"billing:write"})
Enter fullscreen mode Exit fullscreen mode

This is deliberately boring. In an eval harness, I test the deny path as often as the allow path: a billing scope requested by search must fail, and a key from staging must not authenticate to production. The test result belongs in the change record, alongside the project ID and policy revision.

What should an auditable key lifecycle record?

Treat issue, use, rotate, and revoke as different events. Each event should include a timestamp, project ID, environment, key identifier, actor type, actor ID, policy revision, and reason. Store a correlation ID so an alert can lead to the deployment and pull request that changed access. Never put the raw key, authorization header, or complete request body in the log.

A practical event stream looks like this:

Event Required evidence Useful review question
issue requester, approver, scope, expiry Who accepted this blast radius?
use workload, project, environment, key ID Did the caller match its declared project?
rotate old/new key IDs, policy revision, deployment Was the new revision healthy first?
revoke actor, reason, completion timestamp Can we prove the old key stopped working?

The table is a control, not a dashboard decoration. A successful API response does not prove that the access was appropriate. Compare the workload identity, project scope, and environment on every request, then alert on mismatches. OWASP's secrets guidance also treats rotation and access review as lifecycle work rather than a one-time setup.

I once wrote a rotation check that only asserted the new key existed. It passed while the old key remained active. The missing assertion was simple: after the cutover, an authenticated request made with the old identifier must be denied, and the denial must be visible to the audit pipeline. That is the kind of failure a unit test catches before a production review does.

Rotation mechanics that do not depend on developer ownership

Use overlap, then revoke. Create a new key for the project workload, deploy it, perform a health check, and mark the new identifier active. Keep the previous key valid only for a bounded overlap window. Revoke it, record the completion event, and verify the denial. A developer's departure should trigger the same workflow when that developer was an approver, but it should not require replacing every workload key they once touched.

Rotation cadence should follow exposure and privilege. A read-only staging key can have a different interval from a production write key. The policy should also support event-driven rotation after a suspected leak, repository permission change, or ownership transfer. I'm not sure any fixed number of days fits every organization; measure age, scope, access frequency, and incident history, then set an interval your on-call team can actually complete.

Prompt-cost aware teams can keep the audit payload compact: identifiers and decisions are enough for the control plane, while detailed traces stay in a separate, access-controlled store. That keeps an agent or CI log from copying sensitive context into every retry. Your mileage may vary when a provider batches usage or delays revocation visibility, so define a local “revocation confirmed” signal instead of assuming a dashboard is real time.

Trade-offs and a decision rule for multi-project keys

A single shared key is easy to bootstrap, but one leak crosses projects and ownership becomes ambiguous. Per-developer keys improve attribution for interactive tools, yet they create churn for automation and make departures noisy. Project-scoped workload keys usually offer the cleanest boundary for a monorepo because the project remains stable while people move.

The catch is administration. A project registry, approval path, secret manager, and audit store add moving parts. This approach is not suitable when a small script needs temporary access and there is no operator to maintain policy; use a short-lived, manually approved credential there and document the exception. Stick with per-developer credentials when the action is genuinely interactive and your audit system can bind each request to a human.

Before adopting the pattern, run an experiment: seed two projects, attempt cross-project calls, rotate during an in-flight deployment, remove an owner, and replay the audit events. Measure time to revoke, time to attribute a request, denied cross-scope calls, and the number of secrets exposed to each CI job. If those metrics do not improve, adding another key type is ceremony, not security.

The conclusion is modest: stable project identities, narrow scopes, and verifiable lifecycle events beat a naming convention based on developer ownership. Build the registry and tests first; then choose the credential backend that can expose those decisions without hiding them in a vendor-specific console.

References

Top comments (0)