unkin-agent 64d9b89dcd
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Scaffold ghp secrets engine modelled on vault-plugin-secrets-gitea
Mints ephemeral, scoped ghp access tokens via ghp's admin token API
(POST /api/tokens), bound to a Vault lease and revoked on lease
expiry (DELETE /api/tokens/{id}).

- config: base_url + write-only admin_token (ghpsvc_ service token),
  TLS settings; verifies the token is a ghp admin on write. No
  rotate-root: the service token is static and operator-managed.
- roles: token_type (agent/proxy), installation_id, app_record_id,
  repositories, scopes (permission:level), session_prefix, ttl/max_ttl.
- creds: mint a lease-bound token; ghp-side duration bounded by the
  lease ceiling as defence in depth.
- secret ghp_token: idempotent revoke + lease renew.
- Unit tests (config/role/creds/client/scopes/revocation), mock-ghp
  e2e on Vault + OpenBao, Woodpecker pre-commit/build/test/release,
  Makefile patch/minor/major, nfpm RPM packaging.
2026-08-15 19:13:44 +10:00

vault-plugin-secrets-ghp

A Vault / OpenBao secrets engine that mints ephemeral, scoped ghp access tokens on demand.

Why

Machine callers of ghp (the GitHub proxy) — CI identities, agents, ... — should never hold standing ghp tokens: a leaked token is valid until someone notices and revokes it. This engine removes standing tokens entirely:

  • You seed it once with a single ghp service token (ghpsvc_...) that ghp treats as a synthetic site admin.
  • A role binds a ghp App installation (and optional scopes/repositories) to a set of TTLs.
  • Each read of creds/<role> mints a fresh token via POST /api/tokens, bound to a Vault lease, and deletes it from ghp when the lease is revoked or expires (DELETE /api/tokens/{id}).

Why a static service token, not a rotated root credential

ghp service tokens are configured on the ghp server (auth.service_tokens / GHP_AUTH_SERVICE_TOKENS) and authenticate as a synthetic admin with no database row. There is nothing for the engine to rotate in place, so — unlike the sibling vault-plugin-secrets-gitea engine — this backend deliberately has no config/rotate-root path. The service token is supplied and rotated through Vault config by the operator.

Server-side expiry and the Vault lease

Unlike Gitea tokens, ghp tokens do expire server-side. Each mint sets a duration equal to the lease ceiling (max_ttl), so the token self-expires even if lease revocation never reaches ghp. The Vault lease remains the primary expiry: on revoke or max_ttl, the engine issues DELETE /api/tokens/{id}. A delete that returns 404 (token already gone) is treated as success, so revocation is idempotent and Vault's retries converge.

Paths

Path Description
config ghp base_url + TLS settings + seeded admin_token service token. Verifies the token is a ghp admin on write.
roles/<name> Mint policy: token_type, installation_id, app_record_id, repositories, scopes, session_prefix, ttl, max_ttl.
roles List roles.
creds/<role> Read to mint a short-lived, lease-bound ghp token for the role.

Token types

  • agent (default) — backed by a ghp App installation. Requires installation_id; app_record_id optionally pins a specific ghp App record (UUID). This is the natural fit for a service credential and works with the synthetic-admin service token.
  • proxy — OAuth-backed. Included for completeness; note a service-token identity generally has no linked GitHub OAuth token, so proxy mints require ghp to be configured accordingly.

Scopes

scopes is a comma-separated list of permission:level entries (matching ghp's token.ParseScopeString), where level is read or write and permission is a GitHub App permission key (contents, pull_requests, issues, ...). An empty scopes is open-scoped. Likewise an empty repositories grants all repositories in the installation.

Usage

vault secrets enable -path=ghp vault-plugin-secrets-ghp

# Seed with a ghp service token (ghpsvc_...).
vault write ghp/config \
  base_url=https://ghp.unkin.net \
  admin_token='ghpsvc_...' \
  ca_cert=@ghp-ca.pem

# A role that mints 1h agent tokens for App installation 4242, scoped to
# repo contents + PRs.
vault write ghp/roles/agent token_type=agent installation_id=4242 \
  scopes=contents:read,pull_requests:write ttl=1h max_ttl=24h

# Mint one. The token is deleted from ghp when the lease is revoked.
vault read ghp/creds/agent

Development

make build      # build the plugin binary into ./dist
make test       # unit tests (race)
make e2e        # full lifecycle vs mock ghp on Vault + OpenBao (Docker)
make rpm        # build Vault + OpenBao RPMs via nfpm

Releases are tag-driven (make patch|minor|major): a Woodpecker pipeline builds the Vault and OpenBao RPMs and uploads them to the internal artifactapi yum repo.

Deployment (out of scope for this repo; documented for the operator)

Live registration mirrors the sibling vault-plugin-secrets-gitea / vault-plugin-secrets-rancher engines:

  1. Release: tag v0.1.0 (make minor from v0.0.0). CI publishes rpm-internal/files/Packages/{vault,openbao}-plugin-secrets-ghp-<ver>-1.x86_64.rpm.
  2. terraform-vault: plugin catalog entry with the release sha256, engine policies, and ghp_secret_backend + ghp_secret_backend_role module instances (via the companion terraform provider).
  3. Seed the service token in KV before the backend module applies, e.g. kv/service/vault/au/syd1/secret_backend/ghp/config with admin_token set to a ghp service token configured in GHP_AUTH_SERVICE_TOKENS.
  4. Reload after a binary upgrade: vault write sys/plugins/reload/backend plugin=vault-plugin-secrets-ghp. Bump the RPM version in puppet-prod and the catalog sha256 in terraform-vault together so the on-disk binary stays in lockstep with the catalog.
S
Description
HashiCorp Vault / OpenBao secrets engine for ghp: mints ephemeral, scoped access tokens via the ghp admin API, authenticating as a static admin service token
Readme 75 KiB
Languages
Go 86.9%
Shell 8.2%
Makefile 3.8%
HCL 0.8%
Go Template 0.3%