ghp supports HashiCorp Vault as a storage backend, using the KV v2 secrets engine. All data — users, tokens, apps, and credentials — is stored as versioned secrets within a configurable keyspace.
Vault provides encryption at rest natively, so the GHP_ENCRYPTION_KEY
setting is not required when using this backend.
ghp can authenticate to Vault using one of two methods:
- AppRole (default) — role ID and secret ID credentials are provisioned out-of-band and supplied via configuration. Suitable for VMs, bare metal, or any deployment where a Kubernetes-issued JWT is not available.
- Kubernetes — ghp uses the pod's projected service account JWT to authenticate directly to Vault's kubernetes auth backend. Recommended for Kubernetes deployments because it eliminates the need to manage role-id / secret-id credentials and removes Vault Secret Operator (VSO) from the authentication path.
Prerequisites
- HashiCorp Vault 1.12+ with the KV v2 secrets engine enabled
- One of: AppRole auth method enabled, or kubernetes auth method enabled and bound to the cluster ghp runs in
- A dedicated policy granting ghp access to its keyspace
Vault Setup
1. Enable the KV v2 Secrets Engine
If you are using the default secret/ mount (enabled by default in dev mode),
skip this step. Otherwise, enable a dedicated mount:
vault secrets enable -path=ghp-data -version=2 kv
2. Enable an Authentication Method
Choose one of the following based on where ghp runs:
vault auth enable approle
vault auth enable kubernetes
# Or, for multi-cluster setups, mount the backend at a per-cluster path:
vault auth enable -path=kubernetes/my-cluster kubernetes
Configure the auth backend so Vault can validate JWTs issued by the
cluster's API server. Write to auth/<mount>/config matching the path
you enabled above. The recommended pattern is to use Vault's own
service account to call the TokenReview API:
# Default mount:
vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc:443" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
# Custom mount (matching `-path=kubernetes/my-cluster` above):
vault write auth/kubernetes/my-cluster/config \
kubernetes_host="https://kubernetes.default.svc:443" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
See the Vault kubernetes auth docs for full setup details and alternative configurations (e.g. validating JWTs locally without contacting the API server).
3. Create the GHP Policy
ghp needs full CRUD access to its data path and read/list/delete access to metadata (used for key listing and version cleanup):
vault policy write ghp - <<'POLICY'
# Data operations — create, read, update, delete secrets
path "secret/data/ghp/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# Metadata operations — list keys and delete secret versions
path "secret/metadata/ghp/*" {
capabilities = ["read", "list", "delete"]
}
POLICY
Adjust secret and ghp to match your vault_mount and vault_path
configuration if you are not using the defaults.
Least privilege
The policy above grants ghp full control over its keyspace. Do not widen
the paths beyond ghp/* — ghp does not need access to any other Vault
secrets.
4. Create the Vault Role
vault write auth/approle/role/ghp \
policies="ghp" \
token_ttl=1h \
token_max_ttl=4h \
secret_id_ttl=0 \
token_num_uses=0
| Parameter | Recommended | Description |
|---|---|---|
policies |
"ghp" |
The policy created above |
token_ttl |
1h |
How long each issued token is valid before renewal |
token_max_ttl |
4h |
Maximum lifetime before re-authentication is required |
secret_id_ttl |
0 (no expiry) or org policy |
How long the secret ID remains valid |
token_num_uses |
0 (unlimited) |
ghp makes many Vault calls per request |
Bind the Vault role to the Kubernetes service account that ghp's pod
uses. Update bound_service_account_names and
bound_service_account_namespaces to match your deployment.
vault write auth/kubernetes/role/ghp \
bound_service_account_names=ghp \
bound_service_account_namespaces=ghp \
policies=ghp \
token_ttl=1h \
token_max_ttl=4h
| Parameter | Recommended | Description |
|---|---|---|
bound_service_account_names |
ghp |
Service account names allowed to assume this role |
bound_service_account_namespaces |
ghp |
Namespaces those service accounts must live in |
policies |
"ghp" |
The policy created above |
token_ttl |
1h |
How long each issued token is valid before renewal |
token_max_ttl |
4h |
Maximum lifetime before re-authentication is required |
If you mounted the kubernetes backend at a per-cluster path (e.g.
kubernetes/my-cluster), substitute that path in the command above.
ghp automatically re-authenticates when its token expires (up to
token_max_ttl), so short TTLs are safe and recommended.
5. Retrieve Credentials
# Get the role ID (stable, not secret)
vault read auth/approle/role/ghp/role-id
# Generate a secret ID (treat as a secret — store securely)
vault write -f auth/approle/role/ghp/secret-id
For development, you can set fixed credentials instead:
vault write auth/approle/role/ghp/role-id role_id="my-role-id"
vault write auth/approle/role/ghp/custom-secret-id secret_id="my-secret-id"
No credential retrieval is required. ghp authenticates directly using
the projected service account JWT mounted into its pod by Kubernetes
at /var/run/secrets/kubernetes.io/serviceaccount/token.
Ensure the pod runs as the service account bound to the Vault role:
apiVersion: v1
kind: ServiceAccount
metadata:
name: ghp
namespace: ghp
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ghp
namespace: ghp
spec:
template:
spec:
serviceAccountName: ghp
# ...
Custom JWT audience
If the Vault role binds an audience (or audiences) — for example
if you set audience=vault on auth/kubernetes/role/ghp — the
default service account token will not work. The auto-mounted token
at /var/run/secrets/kubernetes.io/serviceaccount/token carries the
API server audience (https://kubernetes.default.svc.cluster.local),
not the audience your role expects, so Vault's login response is:
invalid audience (aud) claim: audience claim does not match any expected audience
The fix is to mount a projected service account token with the desired audience and point ghp at that path. This is the same mechanism Vault Agent uses, and it works without any code changes in ghp:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ghp
namespace: ghp
spec:
template:
spec:
serviceAccountName: ghp
containers:
- name: ghp
# ...
env:
- name: GHP_DATABASE_VAULT_K8S_TOKEN_PATH
value: /var/run/secrets/vault/token
volumeMounts:
- name: vault-token
mountPath: /var/run/secrets/vault
readOnly: true
volumes:
- name: vault-token
projected:
sources:
- serviceAccountToken:
audience: vault # match `audience` on the Vault role
expirationSeconds: 3600
path: token
The kubelet rotates the projected token before it expires. ghp re-reads the file from disk on every (re-)login, so rotation is handled transparently without restarts.
GHP Configuration
export GHP_DATABASE_DRIVER=vault
export GHP_DATABASE_VAULT_ADDR=https://vault.example.com:8200
export GHP_DATABASE_VAULT_MOUNT=secret # KV v2 mount path
export GHP_DATABASE_VAULT_PATH=ghp # key prefix within the mount
export GHP_DATABASE_VAULT_AUTH_METHOD=approle # default; can be omitted
export GHP_DATABASE_VAULT_ROLE_ID=<role-id>
export GHP_DATABASE_VAULT_SECRET_ID=<secret-id>
database:
driver: vault
vault_addr: https://vault.example.com:8200
vault_mount: secret # KV v2 mount path
vault_path: ghp # key prefix within the mount
vault_auth_method: approle # default
vault_role_id: "" # set via GHP_DATABASE_VAULT_ROLE_ID env var
vault_secret_id: "" # set via GHP_DATABASE_VAULT_SECRET_ID env var
export GHP_DATABASE_DRIVER=vault
export GHP_DATABASE_VAULT_ADDR=https://vault.example.com:8200
export GHP_DATABASE_VAULT_MOUNT=secret
export GHP_DATABASE_VAULT_PATH=ghp
export GHP_DATABASE_VAULT_AUTH_METHOD=kubernetes
export GHP_DATABASE_VAULT_K8S_ROLE=ghp
# Optional — override only if you mounted the auth backend at a non-default path:
# export GHP_DATABASE_VAULT_K8S_MOUNT=kubernetes/my-cluster
# Optional — override only if your projected token is at a non-standard path
# (the default is /var/run/secrets/kubernetes.io/serviceaccount/token):
# export GHP_DATABASE_VAULT_K8S_TOKEN_PATH=/var/run/secrets/projected/ghp-token
database:
driver: vault
vault_addr: https://vault.example.com:8200
vault_mount: secret
vault_path: ghp
vault_auth_method: kubernetes
vault_k8s_role: ghp
# vault_k8s_mount: kubernetes # default; override for multi-cluster mounts
# vault_k8s_token_path: /var/run/secrets/projected/ghp-token # default is /var/run/secrets/kubernetes.io/serviceaccount/token
| Field | Env Var | Default | Description |
|---|---|---|---|
vault_addr |
GHP_DATABASE_VAULT_ADDR |
— | Vault server address (required) |
vault_mount |
GHP_DATABASE_VAULT_MOUNT |
secret |
KV v2 secrets engine mount path |
vault_path |
GHP_DATABASE_VAULT_PATH |
ghp |
Key prefix within the mount |
vault_auth_method |
GHP_DATABASE_VAULT_AUTH_METHOD |
approle |
Auth method: approle or kubernetes |
vault_role_id |
GHP_DATABASE_VAULT_ROLE_ID |
— | AppRole role ID (required when method=approle) |
vault_secret_id |
GHP_DATABASE_VAULT_SECRET_ID |
— | AppRole secret ID (required when method=approle) |
vault_k8s_role |
GHP_DATABASE_VAULT_K8S_ROLE |
— | Vault role name (required when method=kubernetes) |
vault_k8s_mount |
GHP_DATABASE_VAULT_K8S_MOUNT |
kubernetes |
Auth mount path for the kubernetes backend |
vault_k8s_token_path |
GHP_DATABASE_VAULT_K8S_TOKEN_PATH |
/var/run/secrets/kubernetes.io/serviceaccount/token |
Path to the projected service account JWT. Override when the Vault role binds a custom audience — see Custom JWT audience. |
No encryption key needed
When using driver: vault, the GHP_ENCRYPTION_KEY setting is ignored.
Vault encrypts all data at rest using its own seal mechanism.
Keyspace Layout
ghp organizes data under the configured vault_path prefix. With the default
settings (mount=secret, path=ghp), the keyspace looks like:
secret/data/ghp/
├── apps/
│ └── {app-id} # App record (name, keys, config)
├── users/
│ ├── {user-id} # User record
│ └── by-github-id/
│ └── {github-id} # Index: GitHub ID → user ID
├── github-tokens/
│ ├── {token-id} # Encrypted OAuth token pair
│ └── by-user/
│ └── {user-id} # Index: user ID → token ID
└── proxy-tokens/
├── {token-id} # Proxy token record
├── by-hash/
│ └── {token-hash} # Index: SHA-256 hash → token ID
└── by-user/
└── {user-id}/
└── {token-id} # Index: user's tokens
Index entries are lightweight secrets that store only a pointer (ID) to the actual record. Lookups that use an index require two Vault reads: one for the index and one for the record.
Migrations
The Vault backend does not use SQL migrations. There is no schema to manage —
data is stored as JSON within KV v2 secrets and the structure evolves with the
application. The ghp migrate command is not applicable when using Vault.
Token Lifecycle and Re-authentication
ghp authenticates to Vault at startup using the configured method. The
issued Vault token has a limited TTL (configured via token_ttl on the
role). When the token expires:
- The next Vault operation returns a 403 (permission denied)
- ghp automatically re-authenticates using the stored credentials
- The failed operation is retried with the new token
This is transparent to users and requires no manual intervention.
AppRole
The only scenario requiring operator action is if the secret ID itself
expires (controlled by secret_id_ttl on the role) — in that case,
generate a new secret ID and update the GHP_DATABASE_VAULT_SECRET_ID
environment variable.
Kubernetes
Kubernetes projects a service account token into the pod and rotates it
periodically (typically every hour, controlled by --service-account-extend-token-expiration
on the API server). On every re-authentication, ghp re-reads the token
file from disk before calling auth/<mount>/login, so rotated tokens
are picked up automatically without restarting the pod.
No operator action is required for the kubernetes method beyond the initial
role binding. If the bound service account is deleted or the kubernetes auth
backend's API server CA changes, ghp's next re-authentication will fail —
investigate via the Vault audit log and the ghp_proxy_decision_duration_seconds
metric for elevated github_token_resolution latency.
invalid audience (aud) claim at startup
If the Vault role binds an audience and the pod still mounts the default
service account token, login fails with invalid audience (aud) claim:
audience claim does not match any expected audience. The default token's
aud is the cluster API server, not the Vault role's audience. Mount a
projected service account token with the matching audience and set
vault_k8s_token_path to that file — see
Custom JWT audience.
High Availability
Single Instance
A single ghp instance with Vault works without any special configuration. In-process caches (token resolution, GitHub credentials) reduce Vault round trips on the hot path.
Multi-Instance Deployments
Multiple ghp instances can share the same Vault backend. Each instance authenticates independently with its own AppRole token.
Concurrency limitations
Vault KV v2 does not support atomic read-modify-write operations. While ghp uses in-process mutexes to protect concurrent access within a single instance, cross-instance operations on the same key can race. In practice this only affects scenarios where two instances modify the same record simultaneously (e.g., revoking the same token from two admin sessions). Token resolution (reads) and proxy operations are safe for concurrent multi-instance use.
Monitoring
ghp instruments all Vault operations through the standard proxy decision
pipeline metrics. The github_token_resolution stage timing includes Vault
read latency, making it visible in the
ghp_proxy_decision_duration_seconds histogram.
Monitor your Vault server's own metrics and audit log for:
- Authentication failures (indicates expired or revoked credentials)
- High read/write latency (may indicate Vault overload or network issues)
- Policy denials (indicates misconfigured policy paths)
Backup and Recovery
Vault's own backup mechanisms apply. ghp does not provide Vault-specific backup tooling. Recommended approaches:
- Vault snapshots (
vault operator raft snapshot save) for integrated storage backends - KV export via the Vault CLI or API for the ghp keyspace
- Vault replication (Enterprise) for disaster recovery
To migrate data between backends (e.g., SQLite to Vault), there is currently no built-in migration tool. The data would need to be exported and imported via the ghp API.
Development Setup
A Docker Compose environment is provided for local development with Vault. See the Development Guide for step-by-step instructions.