Skip to content

Drone credential store — secure credentials_ref resolution for repo cloning #574

Description

@aselim31

Drone Credential Store: Secure Resolution of credentials_ref for Repo Cloning

Problem

When the Mothership dispatches a work assignment to a Drone, the assignment includes a credentials_ref field that names a credential the Drone should use to clone the target repository. Today no implementation of this store exists. The only working pattern — seen in repo-clone/script.ps1 and flagged in #557 Bug 7 — embeds the raw PAT directly in the git clone URL:

git clone https://<PAT>@dev.azure.com/org/project/_git/repo ./path

This leaks the secret in two places: the PAT is visible in process args for the lifetime of the clone operation, and git persists it verbatim in .git/config as remote.origin.url. Every subsequent fetch, pull, and push re-exposes it in git subprocess args. Any process on the same machine with access to process enumeration (Get-CimInstance Win32_Process, Procmon) or the worktree directory can read the credential.

Current Behavior Summary

repo-clone/script.ps1 reads AZURE_DEVOPS_PAT from .env.local and constructs the clone URL with the PAT in the userinfo position. There is no abstraction between the secret and the URL — the clone URL is the credential. After the clone, remote.origin.url in .git/config contains the full PAT. Subsequent git fetch and git push operations re-expose it in process args. The credentials_ref field in work queue assignments has no resolution logic — it is declared in the spec (#95) but not implemented anywhere.

Security Exposure Points

  1. Process args leakgit clone https://<PAT>@... makes the PAT visible in Win32_Process.CommandLine to any process on the same host that can enumerate processes. Observed live: the PAT was readable in process args for all five git subprocesses spawned by a single clone.

  2. Persisted secret — git writes the clone URL to .git/config remote.origin.url. The PAT survives the clone and is readable by anyone with filesystem access to the worktree, leaks into git remote -v output, and is re-exposed in subsequent fetch and push subprocess args.

  3. Log surface — tool output that echoes the clone URL or the contents of .env.local propagates the PAT into Mothership event logs and telemetry sinks.

  4. No per-drone isolation — all assignments on all drones use the same .env.local PAT. There is no way to scope a credential to a specific repo host, project, or assignment type.

Proposal

Introduce a local credential store on each Drone. Credentials are stored on the Drone only — their secrets never transit the Mothership or appear in work queue payloads. The Mothership references credentials by name (credentials_ref). The Drone resolves that name to a secret locally and passes it to git via a per-invocation HTTP header that git does not persist anywhere.

This makes credentials:

  • Name-addressable — multiple credential refs can coexist on one drone
  • Drone-local — secrets live on the drone, not in any queue or server payload
  • Non-persistent — git never writes the secret to disk
  • Auditable — which ref was used for which assignment is recorded, without logging the secret

Credential Store Design

Storage backends

Three backends, selected per drone in drone-config.yaml:

Backend Description When to use
env Secret is an environment variable Simple VM / bare-metal deployments
file Encrypted JSON file, secrets referenced by env var name Docker / multi-credential deployments
system OS credential manager (Windows Credential Manager / Linux Secret Service) Shared-host deployments

Credential metadata format (drone-credentials.json)

The file stores metadata only. Secrets are never written to disk — they are read from environment variables at resolution time.

{
  "credentials": [
    {
      "ref": "github-pat-01",
      "type": "pat",
      "host": "github.com",
      "username": "dotbot-drone",
      "secret_env": "GITHUB_PAT_01"
    },
    {
      "ref": "ado-pat-prod",
      "type": "pat",
      "host": "dev.azure.com",
      "username": "dotbot-drone",
      "secret_env": "AZURE_DEVOPS_PAT"
    }
  ]
}

secret_env names the environment variable that holds the actual secret. If AZURE_DEVOPS_PAT_FILE is also set (Docker secrets convention), the store reads the secret from that file path instead.

Resolution flow

Work assignment received
  → credentials_ref: "ado-pat-prod"
  → Resolve-DroneCredential -Ref "ado-pat-prod"
      → load entry from store (metadata only)
      → read secret from $env:AZURE_DEVOPS_PAT
      → return @{ Host = "dev.azure.com"; Username = "dotbot-drone"; Secret = "..." }
  → pass Secret to git via http.extraHeader
  → Secret never touches disk or URL

Git integration — no PAT in URL

Before (current, insecure):

git clone https://<PAT>@dev.azure.com/org/project/_git/repo ./path
# PAT is in process args, persisted in .git/config remote.origin.url

After (this issue):

$cred = Resolve-DroneCredential -Ref $assignment.credentials_ref
$b64  = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(":$($cred.Secret)"))
git -c "http.extraHeader=Authorization: Basic $b64" clone https://dev.azure.com/org/project/_git/repo ./path

# Sanitise remote after clone — ensure no credentials in .git/config
git -C ./path remote set-url origin https://dev.azure.com/org/project/_git/repo

The PAT is passed as a per-invocation header. Git does not persist http.extraHeader values. .git/config remote.origin.url contains only the clean HTTPS URL.

DroneCredentialStore.psm1

Path: profiles/default/systems/runtime/modules/DroneCredentialStore.psm1

Functions

Initialize-DroneCredentialStore -Config <hashtable>
Load credential metadata from the configured backend on Drone startup. Validate that every referenced environment variable exists and warn on missing entries. Does not read or store secret values — only metadata is loaded at init time.

Resolve-DroneCredential -Ref <string>
Look up a credential by ref name. Read the secret from its configured source (env var or _FILE variant). Return @{ Host; Username; Secret }. Throw a typed error if the ref is unknown, the secret env var is missing, or the resolved secret is empty.

Test-DroneCredential -Ref <string>
Validate that a credential can be resolved without returning the secret value. Used in preflight checks before an assignment is accepted, so the Drone can decline assignments it cannot authenticate.

Get-DroneCredentialRefs
Return the list of known ref names (no secrets). Called at poll time so the Mothership work queue can filter assignments to drones that can resolve the required credentials_ref.

Drone startup integration

Initialize-Drone calls Initialize-DroneCredentialStore during startup. If any credentials_ref declared in drone-config.yaml cannot be validated, startup warns but continues — the Drone will decline assignments whose refs fail Test-DroneCredential.

At poll time, the Drone passes its resolvable credential refs:

GET /api/fleet/work-queue/poll?drone_id={id}&credential_refs=github-pat-01,ado-pat-prod

The Mothership only dispatches assignments whose credentials_ref appears in that list.

drone-config.yaml additions

credentials:
  backend: env              # env | file | system
  file_path: /secrets/drone-credentials.json   # only for backend: file
  refs:
    - ref: github-pat-01
      type: pat
      host: github.com
      username: dotbot-drone
      secret_env: GITHUB_PAT_01
    - ref: ado-pat-prod
      type: pat
      host: dev.azure.com
      username: dotbot-drone
      secret_env: AZURE_DEVOPS_PAT

Docker / secrets mount

For containerised Drones, secrets are injected as environment variables using Docker secrets or Kubernetes secretEnv. The credential store reads *_FILE variants using the Docker secrets convention:

# docker-compose.drone.yaml
services:
  drone-1:
    environment:
      - AZURE_DEVOPS_PAT_FILE=/run/secrets/ado_pat
    secrets:
      - ado_pat
secrets:
  ado_pat:
    file: ./secrets/ado_pat.txt

If AZURE_DEVOPS_PAT_FILE is set, the store reads the secret from that file. If both AZURE_DEVOPS_PAT and AZURE_DEVOPS_PAT_FILE are set, the _FILE variant takes precedence.

Migration Plan

Phase 1: Fix repo-clone immediately

Switch repo-clone/script.ps1 from PAT-in-URL to http.extraHeader. This is a self-contained change in the workflow script and fixes #557 Bug 7 without requiring the full credential store. Sanitise remote.origin.url after clone. This phase has no dependencies.

Phase 2: Implement DroneCredentialStore.psm1

Build the credential store module, integrate it into Initialize-Drone, and wire Invoke-DroneAssignment to call Resolve-DroneCredential before any git operation.

Phase 3: Credential-ref filtering at poll time

Extend Get-DroneAssignment to advertise resolvable credential refs. Extend WorkQueueController (server-side, tracked in #95) to filter assignments accordingly.

Phase 4: Docker secrets documentation

Update docker/docker-compose.drone.yaml and defaults/drone-config.example.yaml with the _FILE secrets pattern.

Expected Impact

  • PATs no longer appear in git process args or .git/config on any drone
  • Assignments are only dispatched to drones that can authenticate the required repo
  • Multiple credential refs coexist on a single drone without conflict
  • Secret rotation requires only an environment variable update — no config file edits

Risks

  • Phase 1 changes the auth mechanism for ADO clones in repo-clone. Any other workflow scripts that embed PATs in URLs must be audited and updated too.
  • If a Drone declares a ref in drone-config.yaml but the env var is missing at runtime, assignments will be declined silently unless Test-DroneCredential surfaces the failure clearly.
  • Docker _FILE convention must be documented clearly or drones will be misconfigured in production.

Non-Goals

  • Drone auto-scaling based on work queue depth — the credential store is per-drone and static; auto-scaling (spinning up new drone instances dynamically) is a separate infrastructure concern tracked as a future item

  • Central credential vault managed by the Mothership — secrets stay drone-local

  • Credential rotation automation — rotation is an ops concern, not a framework concern

  • SSH-based authentication — PAT over HTTPS is the current pattern; SSH support can follow separately

Acceptance Criteria

  • git clone no longer embeds a PAT in the URL in any part of the codebase
  • .git/config remote.origin.url contains only a clean HTTPS URL after every clone
  • Resolve-DroneCredential returns the secret without logging it; the return value is never written to any event, log, or telemetry sink
  • A Drone with a missing env var warns at startup and declines assignments for that ref
  • The Mothership work queue only dispatches assignments to drones that advertise the required credentials_ref
  • Phase 1 (repo-clone fix) ships independently of the full credential store

Dependencies

Area

Mothership & Fleet

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    Status
    Inbox

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions