Reviewed-on: #1
vault-plugin-secrets-gitea
A Vault / OpenBao secrets engine that mints ephemeral, scoped Gitea access tokens on demand.
Why
Static Gitea bot users (teabot personalities, CI identities, ...) should never hold long-lived personal access tokens: a leaked token is valid until someone notices and deletes it, and Gitea tokens never expire on their own. This engine removes standing tokens entirely:
- You seed it once with a single Gitea site-admin credential.
- A role binds a target Gitea username to a set of token scopes and TTLs.
- Each read of
creds/<role>mints a fresh token for that user, bound to a Vault lease, and deletes it from Gitea when the lease is revoked or expires.
Why an admin username + password, not an admin token
Gitea's token-management endpoints (POST/DELETE /api/v1/users/{username}/tokens)
are guarded by reqBasicOrRevProxyAuth() — they reject token/bearer auth and
accept only HTTP Basic Auth or reverse-proxy auth
(go-gitea/gitea#21186). The
same routes are also guarded by reqSelfOrAdmin(), so a site admin using
Basic Auth may mint and delete tokens for any user by naming them in the path.
That is the mechanism this engine relies on, which is why the seeded credential
is an admin username + password, not a token.
The Vault lease is the only expiry
Gitea access tokens have no server-side TTL. The Vault lease is therefore the
sole expiry mechanism: when the lease is revoked or reaches max_ttl, the engine
issues DELETE /api/v1/users/{username}/tokens/{id} to remove the token. 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 |
Gitea URL + TLS settings + seeded admin admin_username/admin_password. Verifies the credentials are a site admin on write. |
config/rotate-root |
Rotate the seeded admin password in place (generates a new random password, changes it via the admin API, stores it). |
roles/<name> |
Mint policy: username, scopes, ttl, max_ttl, token_name_prefix. |
roles |
List roles. |
creds/<role> |
Read to mint a short-lived, lease-bound token for the role's user. |
Scopes
scopes is validated against Gitea's scope set (see
models/auth/access_token_scope.go): all, public-only, and the read: /
write: forms of activitypub, admin, misc, notification, organization,
package, issue, repository, user. write: implies read:.
Usage
vault secrets enable -path=gitea vault-plugin-secrets-gitea
# Seed with a Gitea site-admin username + password (Basic Auth).
vault write gitea/config \
gitea_url=https://git.example.com \
admin_username=bot-admin \
admin_password='...' \
ca_cert=@gitea-ca.pem
# Immediately rotate the seeded password so only Vault knows it.
vault write -f gitea/config/rotate-root
# A role that mints 1h tokens for the "teabot" user, scoped to repo + issues.
vault write gitea/roles/teabot username=teabot \
scopes=read:repository,write:issue ttl=1h max_ttl=24h
# Mint one. The token is deleted from Gitea when the lease is revoked.
vault read gitea/creds/teabot
Root rotation requirements & limits
config/rotate-root changes the seeded admin's password via
PATCH /api/v1/admin/users/{admin_username}. Honestly documented constraints:
- The admin must be a local Gitea user (external-auth users can't have their
password changed this way). Gitea requires
login_nameon the edit call; the engine sendsadmin_login_name(default:admin_username) andadmin_source_id(default0, i.e. local). - The admin account must not have TOTP/2FA enabled — Basic Auth with 2FA requires an OTP header the engine cannot supply.
- Rotation changes Gitea first, then persists to Vault. If the persist fails the
engine rolls the password back. If both the persist and the rollback fail
(extremely unlikely), the response says so — reset the admin password manually
and re-seed
config.
Development
make build # build the plugin binary into ./dist
make test # unit tests (race)
make e2e # full lifecycle vs mock Gitea 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 in the real cluster is done exactly like the sibling
vault-plugin-secrets-rancher engine. The steps, in order:
- Repos: the Gitea repo is provisioned via
terraform-git(config/git.unkin.net/unkin/repository/vault-plugin-secrets-gitea.yaml) → PR → plan/apply. After the repo auto-inits, enable its Woodpecker webhook manually in the Woodpecker UI (new repos get no webhook automatically) so the requiredpre-commit/build/testchecks run. - Release: tag
v0.1.0(make minorfromv0.0.0). CI publishesrpm-internal/files/Packages/{vault,openbao}-plugin-secrets-gitea-<ver>-1.x86_64.rpm. - terraform-vault (mirrors the rancher wiring — merge in this order):
policies/gitea/admin.yaml— deployer catalog + engine grant.config/plugins/vault-plugin-secrets-gitea.yaml— plugin catalog entry with the releasesha256.puppet-prod: addopenbao-plugin-secrets-gitea(pinned to the exact version) toprofiles::packages::includeon the vault storage role.gitea_secret_backend+gitea_secret_backend_rolemodule instances and their config yamls (via the companionterraform-provider-giteavaultsecret).
- Seed the admin credential in KV before the backend module applies, e.g.
kv/service/vault/au/syd1/secret_backend/gitea/configwithadmin_username+admin_passwordfor a purpose-built Gitea site-admin bot account (2FA disabled). Then runvault write -f gitea/config/rotate-rootso the standing seed password is replaced by one only Vault holds. - Reload after a binary upgrade:
vault write sys/plugins/reload/backend plugin=vault-plugin-secrets-gitea(thevault plugin reload -plugin=…CLI form is broken on this OpenBao). Bump the RPM version in puppet-prod and the catalogsha256in terraform-vault together so the on-disk binary stays in lockstep with the catalog.