Initial vault-plugin-secrets-rancher scaffold
Vault/OpenBao secrets engine managing Rancher API tokens via the public tokens.ext.cattle.io API. - config: Rancher connection (URL + TLS) - service-accounts/<name>: seeded root tokens, auto-rotated before Rancher's TTL cap via a PeriodicFunc (default 45d rotation, 90d token TTL); the current token mints its own replacement. Manual /rotate endpoint too. - roles/<name>: mint policy referencing a service account; cluster_name + TTL scoping (Rancher tokens inherit the seeding user's RBAC). - creds/<role>: dynamic, lease-bound tokens deleted from Rancher on revoke. Ports the bind-tsig Woodpecker RPM release, nfpm packaging, and a mock-Rancher e2e (Vault + OpenBao). Unit tests cover the full lifecycle.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
package rancher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
const rancherTokenType = "rancher_token"
|
||||
|
||||
func (b *rancherBackend) rancherTokenSecret() *framework.Secret {
|
||||
return &framework.Secret{
|
||||
Type: rancherTokenType,
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"token": {
|
||||
Type: framework.TypeString,
|
||||
Description: "The Rancher API token value.",
|
||||
},
|
||||
"token_name": {
|
||||
Type: framework.TypeString,
|
||||
Description: "The ext.cattle.io Token resource name.",
|
||||
},
|
||||
"cluster_name": {
|
||||
Type: framework.TypeString,
|
||||
Description: "The cluster the token is scoped to (empty = full scope).",
|
||||
},
|
||||
},
|
||||
Revoke: b.secretRevoke,
|
||||
Renew: b.secretRenew,
|
||||
}
|
||||
}
|
||||
|
||||
// secretRevoke deletes the minted Rancher token via the ext.cattle.io API, using
|
||||
// the service account that issued it.
|
||||
func (b *rancherBackend) secretRevoke(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
||||
tokenName, err := internalString(req.Secret.InternalData, "token_name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
saName, err := internalString(req.Secret.InternalData, "service_account")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sa, err := b.getServiceAccount(ctx, req.Storage, saName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sa == nil {
|
||||
// The issuing service account is gone; nothing left that could revoke
|
||||
// the token here. Rancher will expire it at its own TTL.
|
||||
return nil, fmt.Errorf("service account %q no longer exists; cannot revoke token %q", saName, tokenName)
|
||||
}
|
||||
|
||||
client, err := b.clientFor(ctx, req.Storage, sa.Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := client.DeleteToken(ctx, tokenName); err != nil {
|
||||
return nil, fmt.Errorf("revoking rancher token %q: %w", tokenName, err)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// secretRenew extends the Vault lease; the token material is unchanged. Note the
|
||||
// Rancher-side token TTL is fixed at creation, so a renewed lease should stay
|
||||
// within the token's original TTL (bounded by the role max_ttl).
|
||||
func (b *rancherBackend) secretRenew(_ context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
||||
return &logical.Response{Secret: req.Secret}, nil
|
||||
}
|
||||
|
||||
func internalString(data map[string]interface{}, key string) (string, error) {
|
||||
raw, ok := data[key]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("secret is missing internal %s data", key)
|
||||
}
|
||||
s, ok := raw.(string)
|
||||
if !ok {
|
||||
return "", errors.New("secret internal " + key + " data is not a string")
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
Reference in New Issue
Block a user