Files
vault-plugin-secrets-gitea/backend.go
T
unkinben 20613afb26 Initial vault-plugin-secrets-gitea engine
Add a Vault/OpenBao secrets engine that mints ephemeral, scoped Gitea
access tokens on demand. The engine holds a single seeded Gitea site-admin
Basic-Auth credential and, per role, mints a fresh per-user token via the
admin API, bound to a Vault lease and deleted from Gitea on revocation.
Gitea requires Basic Auth for token management (token auth is rejected),
and reqSelfOrAdmin lets a site admin manage any user's tokens, which is the
mechanism this relies on. Gitea tokens never expire server-side, so the
Vault lease is the sole expiry mechanism.

- add backend wiring, config (+ rotate-root), roles, creds paths
- add the gitea client (Basic Auth create/delete token, admin password change)
- add scope validation against Gitea's access-token scope set
- add unit tests (fake Gitea API) and a Vault+OpenBao e2e harness
- add Makefile, nfpm RPM packaging, and Woodpecker build/test/release pipelines

Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
2026-07-27 00:54:59 +10:00

107 lines
3.7 KiB
Go

// Package gitea implements a Vault / OpenBao secrets engine that mints
// ephemeral, scoped Gitea personal access tokens on demand.
//
// Static Gitea bot users (teabot personalities, CI identities, ...) should never
// hold long-lived tokens. The engine is seeded with a single Gitea *site admin*
// credential (username + password, HTTP Basic Auth) and, on each read of
// creds/<role>, mints a fresh access token for the role's target user via the
// admin API (POST /api/v1/users/{username}/tokens). Each minted token is bound
// to a Vault lease and deleted from Gitea (DELETE .../tokens/{id}) when the
// lease expires or is revoked.
//
// Why Basic Auth and not an admin token: Gitea's token-management endpoints are
// guarded by reqBasicOrRevProxyAuth() — you cannot create or delete a token
// using token/bearer auth, only Basic Auth or reverse-proxy auth (see
// go-gitea/gitea#21186). The same routes are guarded by reqSelfOrAdmin(), so a
// site admin authenticating with Basic Auth may mint and delete tokens for *any*
// user by naming them in the path. That is exactly the mechanism this engine
// relies on, which is why the seeded credential is an admin username+password.
//
// Gitea access tokens have no server-side expiry: once created a token lives
// until it is deleted. The Vault lease is therefore the *only* expiry mechanism
// — lease revocation deletes the token, and that is what bounds its lifetime.
package gitea
import (
"context"
"errors"
"strings"
"sync"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
// errBackendNotConfigured is returned when a credential is requested before the
// Gitea connection has been configured.
var errBackendNotConfigured = errors.New("gitea backend not configured; write config first")
type giteaBackend struct {
*framework.Backend
// lock serialises root-credential rotation against credential issuance so a
// mint never races a password change out from under it.
lock sync.RWMutex
}
// Factory returns a configured Gitea secrets backend.
func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
b := backend()
if err := b.Setup(ctx, conf); err != nil {
return nil, err
}
return b, nil
}
func backend() *giteaBackend {
b := &giteaBackend{}
b.Backend = &framework.Backend{
Help: strings.TrimSpace(backendHelp),
BackendType: logical.TypeLogical,
PathsSpecial: &logical.Paths{
SealWrapStorage: []string{configStoragePath},
},
Paths: framework.PathAppend(
[]*framework.Path{
pathConfig(b),
pathConfigRotateRoot(b),
pathRole(b),
pathRolesList(b),
pathCredentials(b),
},
),
Secrets: []*framework.Secret{
b.giteaTokenSecret(),
},
}
return b
}
// clientFor builds a Gitea client from the stored config, authenticated with the
// seeded admin Basic-Auth credentials.
func (b *giteaBackend) clientFor(ctx context.Context, s logical.Storage) (*giteaClient, error) {
config, err := getConfig(ctx, s)
if err != nil {
return nil, err
}
if config == nil {
return nil, errBackendNotConfigured
}
return newClient(config)
}
const backendHelp = `
The gitea secrets engine mints ephemeral, scoped Gitea access tokens.
Seed the engine with a Gitea site-admin username and password (Basic Auth);
Gitea only permits token management via Basic Auth, and a site admin may manage
tokens for any user. Roles bind 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 deleted from Gitea on revocation. Gitea tokens never expire
server-side, so the Vault lease is the only thing that bounds their lifetime.
Use config/rotate-root to rotate the seeded admin password in place.
`