Enterprise use: identity, policy and revocation
Identity, policy and revocation through the providers: who decides what, the workload identity set-up for GitHub Actions, Kubernetes and VMs, what a revocation reaches, and checking what an identity may read.
SourceOrrery has no server of its own. A team that needs one identity per workload, roles, centrally administered policy and revocation gets them from the secrets service a vault’s connection keeps its secrets at: HashiCorp Vault, AWS Secrets Manager or Azure Key Vault. SourceOrrery’s part is narrower and is what this page describes:
- with ambient credentials, it authenticates as the process using the provider’s credential chain (a CI job’s OIDC token, a pod’s service account or a VM’s instance role); a connection can instead use explicit credentials sealed in the local vault;
- it carries every decision the provider makes through unchanged, as the same stable error codes and hints in the CLI, the Rust client and every SDK;
- it does not persist or share a provider-value cache. Values explicitly read by an application or delivered to a child remain that process’s responsibility after provider access is revoked.
The provider-vault, provider-aws and provider-azure CI jobs exercise
these paths against a real Vault server and Secrets Manager and Key Vault
emulators (scripts/ci/vault-federation.sh, scripts/ci/aws-federation.sh,
scripts/ci/azure-federation.sh). Emulators do not establish production
identity, policy propagation or hosted-service behaviour.
Who decides what
Section titled “Who decides what”| HashiCorp Vault | AWS Secrets Manager | Azure Key Vault | SourceOrrery | |
|---|---|---|---|---|
| Identity | an auth method: token, AppRole, JWT/OIDC, Kubernetes | an IAM principal: user, role, web identity, instance or task role | an Entra ID principal: service principal, workload identity, managed identity, a signed-in user | finds the credential where the provider’s own tools do; refuses to run without one (unauthenticated) |
| Roles | the policies attached to the login role | the IAM role the identity assumes | role assignments on the vault (Key Vault Secrets User, Key Vault Secrets Officer), or the vault’s access policies | none of its own; the process is whatever the provider says it is |
| Policy | ACL policies on <mount>/data/<name> and <mount>/metadata/<name>; update on metadata is policy administration (the version flags and windows live there) |
IAM policies on the secret’s ARN (secretsmanager:GetSecretValue, DescribeSecret, ListSecretVersionIds, …); TagResource is policy administration (the flags and windows live in tags) |
data actions on the vault’s secrets; the flags and windows are each version’s own attributes, and the one tag SourceOrrery relies on (version order) needs the permission that writes versions | evaluates the flags and windows it reads from the provider; cannot tell a planted one from a set one (threat model, gap 13) |
| Revocation | vault token revoke, a lease that expires, a disabled role or auth mount |
a revoked session, a removed trust policy, a deny statement | a removed role assignment, a disabled service principal, a deleted federated credential | takes effect on the next request (below) |
| Audit | the server’s audit devices | CloudTrail | the vault’s diagnostic logs in Azure Monitor | the vault records every call it sends to a provider in its own log, with the provider’s outcome; sourceorrery audit also prints where the provider’s trail is and the query that shows SourceOrrery’s operations; the vault’s log is exported with audit export (below) |
A secret in the vault file has no identity beyond the OS account that can open it: anyone who can read the file and reach this machine’s key, or hold the recovery key, reads every such secret. Enterprise use means keeping secrets at a provider, through a connection whose credentials are the workload’s own (this machine’s sign-in), so the provider decides who reads what.
Federation: the identity the platform already has
Section titled “Federation: the identity the platform already has”Provider settings belong to the local vault’s connection, configured by
setup or vault create flags. sourceorrery.toml names the project’s
local vault and secret references only; it contains no provider address,
mount, prefix or credentials. With ambient sign-in, credentials come from
the provider’s chain for the process. Explicit credentials are supplied
from files when configuring the connection and are sealed in the vault.
HashiCorp Vault
Section titled “HashiCorp Vault”The provider tries, in order: VAULT_TOKEN; ~/.vault-token (what
vault login writes); AppRole (VAULT_ROLE_ID and VAULT_SECRET_ID);
JWT (VAULT_AUTH_ROLE with VAULT_JWT_FILE or VAULT_JWT); Kubernetes
(VAULT_AUTH_ROLE in a pod, with the service account token at
/var/run/secrets/kubernetes.io/serviceaccount/token). VAULT_AUTH_MOUNT
names the auth method’s mount when it is not approle, jwt or
kubernetes; VAULT_NAMESPACE and VAULT_CACERT mean what they mean to
the vault CLI. A login happens once, when the vault is opened; the token
it returns is used for the life of the client and is not renewed, so a
process that outlives the role’s token TTL gets unauthenticated on its
next request and has to open the vault again.
GitHub Actions. The job’s OIDC token is a JWT Vault can verify against GitHub’s discovery document. Once, as a Vault administrator:
vault auth enable jwtvault write auth/jwt/config \ oidc_discovery_url="https://token.actions.githubusercontent.com" \ bound_issuer="https://token.actions.githubusercontent.com"vault policy write ci-read - <<'HCL'path "secret/data/database-password" { capabilities = ["read"] }path "secret/metadata/database-password" { capabilities = ["read"] }HCLvault write auth/jwt/role/ci \ role_type=jwt user_claim=sub bound_audiences=vault token_policies=ci-read \ bound_claims='{"repository":"example/app"}' token_ttl=10mIn the workflow, with permissions: id-token: write:
- run: | curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=vault" \ | python3 -c 'import json,sys; print(json.load(sys.stdin)["value"], end="")' > "$RUNNER_TEMP/vault.jwt" echo "VAULT_JWT_FILE=$RUNNER_TEMP/vault.jwt" >> "$GITHUB_ENV" echo "VAULT_AUTH_ROLE=ci" >> "$GITHUB_ENV"- run: sourceorrery check --access && sourceorrery run -- ./deployThe CI job proves this path with a key pair minted on the runner in place
of GitHub’s: a token signed by it logs in and reads, a token for another
audience is unauthenticated.
Kubernetes. Either the Kubernetes auth method, bound to the service account and namespace of the workload, or the JWT method with a projected service account token (its audience and expiry are the cluster’s to set):
env: - { name: VAULT_AUTH_ROLE, value: app } # auth/kubernetes/role/app # or, for a projected token at auth/jwt: # - { name: VAULT_JWT_FILE, value: /var/run/secrets/tokens/vault }VMs and bare metal. The provider has no cloud auth methods of its own
(Vault’s aws, gcp and azure methods). Use AppRole, with the secret
id delivered by the configuration management that places the service, or
log in with the vault CLI (vault login -method=aws, for instance): the
token it writes to ~/.vault-token is the second source the provider
tries. In both cases the token’s TTL is the policy’s lever over a
long-running process.
AWS Secrets Manager
Section titled “AWS Secrets Manager”The official SDK’s default credential chain supplies the identity:
environment variables, the shared config with AWS_PROFILE, SSO, web
identity (AWS_WEB_IDENTITY_TOKEN_FILE and AWS_ROLE_ARN), the ECS task
role, the EC2 instance role. Credentials are resolved when the vault is
opened: no identity at all is unauthenticated before any request is
made.
GitHub Actions. aws-actions/configure-aws-credentials with a
role-to-assume exchanges the job’s OIDC token at STS and exports the
session to the job’s environment; nothing else is needed. The same
exchange without the action is the two web identity variables:
- run: | curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" \ | python3 -c 'import json,sys; print(json.load(sys.stdin)["value"], end="")' > "$RUNNER_TEMP/aws.jwt" echo "AWS_WEB_IDENTITY_TOKEN_FILE=$RUNNER_TEMP/aws.jwt" >> "$GITHUB_ENV" echo "AWS_ROLE_ARN=arn:aws:iam::123456789012:role/ci-read" >> "$GITHUB_ENV"- run: sourceorrery check --access && sourceorrery run -- ./deployThe CI job proves the web identity path against the emulator’s STS, and
that a process with no credentials (and AWS_EC2_METADATA_DISABLED=true)
is refused before it sends a request.
Kubernetes. IAM roles for service accounts (IRSA) inject the two web identity variables into the pod; EKS Pod Identity serves credentials from the container credential endpoint. Both are part of the default chain, so a pod needs no configuration beyond its service account annotation.
VMs and containers. The EC2 instance profile and the ECS task role are found through their metadata endpoints with nothing set. Static access keys in the environment work too, and are the one path on this page that stores a credential; prefer a role.
Azure Key Vault
Section titled “Azure Key Vault”The provider signs in with the first identity the environment provides: a
service principal (AZURE_TENANT_ID, AZURE_CLIENT_ID and
AZURE_CLIENT_SECRET), workload identity federation
(AZURE_FEDERATED_TOKEN_FILE with the tenant and client ids), the Azure CLI
or Azure Developer CLI, or a managed identity
(details). A token is fetched
when the vault is opened: no identity at all is unauthenticated before any
request reaches the vault. The identity needs Key Vault Secrets User on
the vault to read (Key Vault Secrets Officer to write).
GitHub Actions. Give the app registration (or a user-assigned managed
identity) a federated credential for the repository and branch (subject
repo:<owner>/<repo>:ref:refs/heads/main, audience
api://AzureADTokenExchange), assign it the role on the vault, and write
the job’s OIDC token to a file:
- run: | curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=api://AzureADTokenExchange" \ | python3 -c 'import json,sys; print(json.load(sys.stdin)["value"], end="")' > "$RUNNER_TEMP/azure.jwt" echo "AZURE_FEDERATED_TOKEN_FILE=$RUNNER_TEMP/azure.jwt" >> "$GITHUB_ENV" echo "AZURE_TENANT_ID=00000000-0000-0000-0000-000000000000" >> "$GITHUB_ENV" echo "AZURE_CLIENT_ID=11111111-1111-1111-1111-111111111111" >> "$GITHUB_ENV"- run: sourceorrery check --access && sourceorrery run -- ./deployazure/login with OIDC signs the Azure CLI in instead, which the provider
finds too. The CI job proves against the emulator that a service principal
from the environment reads, that a principal without a role on the vault is
refused as permission_denied, and that a process with no identity is
refused before it sends a request. The emulator does not exchange federated
tokens, so the workload identity path itself is exercised only by the Azure
SDK’s own tests.
Kubernetes. AKS workload identity injects AZURE_CLIENT_ID,
AZURE_TENANT_ID, AZURE_FEDERATED_TOKEN_FILE and AZURE_AUTHORITY_HOST
into a pod whose service account names the identity; nothing else is needed.
VMs and containers. A virtual machine’s managed identity is found
through the instance metadata endpoint with nothing set (AZURE_CLIENT_ID
names a user-assigned one); App Service, Functions and Container Apps
provide IDENTITY_ENDPOINT. A client secret in the environment works too,
and is the one path here that stores a credential; prefer a managed identity
or federation.
What revocation reaches
Section titled “What revocation reaches”A revoked identity is refused the next time the provider is asked anything. What that means for a process depends on where it is in its life:
- A process that has not yet read a remote secret is refused when
the provider checks its identity. Opening the local vault alone does
not prove access to a provider. If a required secret cannot be read,
sourceorrery rundoes not start its child. - A process holding an open reader fails on its next read. The Vault provider keeps the token its login returned and does not re-login; the AWS provider keeps the session the chain resolved; the Azure provider keeps signing in as the identity that first gave it a token, and Key Vault checks the role on every request, so a removed role assignment refuses the next read once it has propagated (within minutes). None retries with another identity.
- A value already read stays with the process that read it until it
drops it. There is no shared or persistent provider-value cache: every
remote
get_secretmakes a provider request.runexplicitly delivers values to its child’s environment; a reveal can also be redirected to a file by its caller. An SDK value buffer is zeroised when the application closes or drops it; copies made by the application have their own lifetime (threat model).
After the provider has applied a revocation, subsequent requests under
that identity are refused. Propagation and token caching depend on the
provider. Revocation cannot recall a value already in a
process’s memory or in a child’s environment. Rotating the secret is what
invalidates those (rotation.md: sourceorrery secret rotate
issues, validates and activates a new version and retires the previous
credential at the issuer).
Collecting the audit trail
Section titled “Collecting the audit trail”There is no SourceOrrery server to collect audit logs into, so collection means getting every trail to the place the organisation already keeps logs:
- A local vault keeps a keyed hash chain of metadata-only events
(threat model).
sourceorrery audit exportwrites it oldest first as JSON Lines on stdout, or posts it as OTLP/HTTP log records to a collector (--format otlp --endpoint URL, or the standardOTEL_EXPORTER_OTLP_*variables), and ends with the chain verification as a trailer, so the collector can tell a complete export from a truncated log;--followkeeps exporting new events as they are recorded. A sidecar that runssourceorrery audit export --follow --format otlpnext to the application, with the vault’s recovery key fromSOURCEORRERY_RECOVERY_KEY_FILE(and its view-check password fromSOURCEORRERY_PASSWORD_FILE, when it has one), is the whole set-up; so is a cron job with--sinceand afilesource on the redirected output (cli.md). - A provider’s trail records SourceOrrery’s operations as the
provider’s own API calls, with the identity the process authenticated
as (Vault’s audit devices, CloudTrail, Azure Monitor), besides the
vault’s own record of each call.
sourceorrery auditprints where the trail is and the query that shows those operations; each provider page has the full form under “Audit trail” (HashiCorp Vault, AWS Secrets Manager). Route those logs to the same collector and the two trails meet there.
Checking what an identity may read
Section titled “Checking what an identity may read”sourceorrery check reads metadata only: it says whether every secret
the manifest names resolves to an accessible version, without a value
leaving the provider. A policy can allow that and still deny the value
(Vault’s metadata path without data; IAM DescribeSecret without
GetSecretValue), which is the right shape for an operator’s identity and
the wrong one for the application’s. check --access adds one read per
secret, discarded at once, so the status is permission_denied exactly
where the application’s run would fail:
$ sourceorrery check --access -o json{ "manifest": "/srv/app/sourceorrery.toml", "ok": false, "secrets": [ { "name": "database-password", "env": "DATABASE_PASSWORD", "reference": "sourceorrery://production/database-password", "required": true, "status": "permission_denied", "version": null } ]}The exit status is 1 while a required secret is not readable, so the command is a pre-flight step a deployment can stop on.
On a local vault the extra read is an audited secret_get like any other.
Error codes and hints
Section titled “Error codes and hints”| Code | When | The hint names |
|---|---|---|
unauthenticated |
no credential found; a token or session the provider refuses; an expired login | the provider’s credential sources (VAULT_TOKEN, vault login, the AWS chain), or storing credentials in the vault’s connection |
permission_denied |
the provider’s policy denies the call | Vault: the data and metadata paths to grant with vault policy write and vault read sys/capabilities-self; AWS: the secretsmanager:* actions to allow and aws sts get-caller-identity |
provider_unavailable |
the provider cannot be reached or fails internally (transient) | reachability and the address in the vault’s connection; retry |
outcome_unknown |
a change was sent and the provider may have made it (its answer was lost, or the vault could not record it) | look at the secret at the provider before repeating the change; sourceorrery audit lists such changes (providers) |
The codes are the core’s and are append-only; an SDK older than a code reports it as its base error with the code preserved (ADR 0007).