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
-
Process args leak — git 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.
-
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.
-
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.
-
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
Drone Credential Store: Secure Resolution of
credentials_reffor Repo CloningProblem
When the Mothership dispatches a work assignment to a Drone, the assignment includes a
credentials_reffield 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 inrepo-clone/script.ps1and flagged in #557 Bug 7 — embeds the raw PAT directly in the git clone URL: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/configasremote.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.ps1readsAZURE_DEVOPS_PATfrom.env.localand 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.urlin.git/configcontains the full PAT. Subsequentgit fetchandgit pushoperations re-expose it in process args. Thecredentials_reffield in work queue assignments has no resolution logic — it is declared in the spec (#95) but not implemented anywhere.Security Exposure Points
Process args leak —
git clone https://<PAT>@...makes the PAT visible inWin32_Process.CommandLineto 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.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 intogit remote -voutput, and is re-exposed in subsequent fetch and push subprocess args.Log surface — tool output that echoes the clone URL or the contents of
.env.localpropagates the PAT into Mothership event logs and telemetry sinks.No per-drone isolation — all assignments on all drones use the same
.env.localPAT. 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:
Credential Store Design
Storage backends
Three backends, selected per drone in
drone-config.yaml:envfilesystemCredential 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_envnames the environment variable that holds the actual secret. IfAZURE_DEVOPS_PAT_FILEis also set (Docker secrets convention), the store reads the secret from that file path instead.Resolution flow
Git integration — no PAT in URL
Before (current, insecure):
After (this issue):
The PAT is passed as a per-invocation header. Git does not persist
http.extraHeadervalues..git/config remote.origin.urlcontains only the clean HTTPS URL.DroneCredentialStore.psm1
Path:
profiles/default/systems/runtime/modules/DroneCredentialStore.psm1Functions
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
_FILEvariant). 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-DroneCredentialRefsReturn 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-DronecallsInitialize-DroneCredentialStoreduring startup. If anycredentials_refdeclared indrone-config.yamlcannot be validated, startup warns but continues — the Drone will decline assignments whose refs failTest-DroneCredential.At poll time, the Drone passes its resolvable credential refs:
The Mothership only dispatches assignments whose
credentials_refappears in that list.drone-config.yaml additions
Docker / secrets mount
For containerised Drones, secrets are injected as environment variables using Docker secrets or Kubernetes
secretEnv. The credential store reads*_FILEvariants using the Docker secrets convention:If
AZURE_DEVOPS_PAT_FILEis set, the store reads the secret from that file. If bothAZURE_DEVOPS_PATandAZURE_DEVOPS_PAT_FILEare set, the_FILEvariant takes precedence.Migration Plan
Phase 1: Fix repo-clone immediately
Switch
repo-clone/script.ps1from PAT-in-URL tohttp.extraHeader. This is a self-contained change in the workflow script and fixes #557 Bug 7 without requiring the full credential store. Sanitiseremote.origin.urlafter clone. This phase has no dependencies.Phase 2: Implement DroneCredentialStore.psm1
Build the credential store module, integrate it into
Initialize-Drone, and wireInvoke-DroneAssignmentto callResolve-DroneCredentialbefore any git operation.Phase 3: Credential-ref filtering at poll time
Extend
Get-DroneAssignmentto advertise resolvable credential refs. ExtendWorkQueueController(server-side, tracked in #95) to filter assignments accordingly.Phase 4: Docker secrets documentation
Update
docker/docker-compose.drone.yamlanddefaults/drone-config.example.yamlwith the_FILEsecrets pattern.Expected Impact
.git/configon any droneRisks
repo-clone. Any other workflow scripts that embed PATs in URLs must be audited and updated too.drone-config.yamlbut the env var is missing at runtime, assignments will be declined silently unlessTest-DroneCredentialsurfaces the failure clearly._FILEconvention 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 cloneno longer embeds a PAT in the URL in any part of the codebase.git/config remote.origin.urlcontains only a clean HTTPS URL after every cloneResolve-DroneCredentialreturns the secret without logging it; the return value is never written to any event, log, or telemetry sinkcredentials_refDependencies
Area
Mothership & Fleet