Files
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

115 lines
4.9 KiB
Markdown

# vault-plugin-secrets-ghp
A Vault / OpenBao secrets engine that mints **ephemeral, scoped ghp access
tokens** on demand.
## Why
Machine callers of [ghp](https://git.unkin.net/unkin/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
```sh
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
```sh
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.