Luke Granger-Brown
7592e76a31
tokend is responsible for issuing service-scoped tokens based on the token held and generated by the Vault Agent. It can also generate "server-user" scoped tokens, which exist for convenience's sake: they are not a strong attestation of the user on the machine, and have limited privileges compared to a Vault token issued using e.g. `vault login -method=oidc`.
106 lines
3.3 KiB
Go
106 lines
3.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"regexp"
|
|
"time"
|
|
|
|
log "github.com/golang/glog"
|
|
vapi "github.com/hashicorp/vault/api"
|
|
)
|
|
|
|
var (
|
|
vaultTokenTTL = flag.String("vault_token_ttl", "20m", "TTL at which token must be renewed.")
|
|
vaultTokenMaxTTL = flag.String("vault_token_max_ttl", "1d", "TTL at which token must be reissued.")
|
|
vaultTokenRenewable = flag.Bool("vault_token_renewable", true, "Whether the tokens are renewable or reissued every time.")
|
|
)
|
|
|
|
type vaultTokenInteractor struct {
|
|
v *vapi.Client
|
|
}
|
|
|
|
var _ tokenInteractor = ((*vaultTokenInteractor)(nil))
|
|
|
|
func (i *vaultTokenInteractor) Revoke(ctx context.Context, tokenSecret *TokenSecret) error {
|
|
// Use the token, since we have it, to revoke itself.
|
|
c, err := i.v.Clone()
|
|
if err != nil {
|
|
return fmt.Errorf("generating a new Vault client: %w", err)
|
|
}
|
|
|
|
token, err := tokenSecret.TokenID()
|
|
if err != nil {
|
|
return fmt.Errorf("getting token from secret: %w", err)
|
|
}
|
|
|
|
c.SetToken(token)
|
|
|
|
// Since we have the token, we could potentially use /revoke, or even /revoke-self.
|
|
if err := c.Auth().Token().RevokeSelf(""); err != nil {
|
|
return fmt.Errorf("revoking token: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func computePolicies(selfPolicies []string, username string, isPlainUser bool) []string {
|
|
appMatchRE := regexp.MustCompile(fmt.Sprintf(`^(server/[^/]+/)?app/%s$`, regexp.QuoteMeta(username)))
|
|
userMatchRE := regexp.MustCompile(fmt.Sprintf(`^(server/[^/]+/user|server-user)(/%s)?$`, regexp.QuoteMeta(username)))
|
|
|
|
var outPolicies []string
|
|
for _, p := range selfPolicies {
|
|
if p == "default" || appMatchRE.MatchString(p) || (isPlainUser && userMatchRE.MatchString(p)) {
|
|
outPolicies = append(outPolicies, p)
|
|
}
|
|
}
|
|
return outPolicies
|
|
}
|
|
|
|
func policiesForToken(ts *TokenSecret) ([]string, error) {
|
|
var ps []string
|
|
psIntf, ok := ts.Data["policies"].([]interface{})
|
|
if !ok {
|
|
return nil, fmt.Errorf("policies not present or not expected []interface{} type (was %T)", ts.Data["policies"])
|
|
}
|
|
for _, p := range psIntf {
|
|
ps = append(ps, p.(string))
|
|
}
|
|
return ps, nil
|
|
}
|
|
|
|
func (i *vaultTokenInteractor) Issue(ctx context.Context, username string, isPlainUser bool) (*TokenSecret, error) {
|
|
// Look up our own token to work out what policies we might want to apply.
|
|
// TODO: maybe consider caching this so we don't need to do it every time?
|
|
self, err := i.v.Auth().Token().LookupSelf()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("looking up server-wide token: %w", err)
|
|
}
|
|
selfPolicies, err := policiesForToken(self)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("retrieving self policies: %w", err)
|
|
}
|
|
|
|
wantPolicies := computePolicies(selfPolicies, username, isPlainUser)
|
|
log.Infof("policies for %v: %v", username, wantPolicies)
|
|
|
|
return i.v.Auth().Token().Create(&vapi.TokenCreateRequest{
|
|
// TODO: extend rather than replace metadata?
|
|
Metadata: map[string]string{
|
|
"app": username,
|
|
},
|
|
TTL: *vaultTokenTTL,
|
|
ExplicitMaxTTL: *vaultTokenMaxTTL,
|
|
Renewable: vaultTokenRenewable,
|
|
Policies: wantPolicies,
|
|
})
|
|
}
|
|
|
|
func (i *vaultTokenInteractor) Renew(ctx context.Context, tokenSecret *TokenSecret) (*TokenSecret, error) {
|
|
token, err := tokenSecret.TokenID()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting token from secret: %w", err)
|
|
}
|
|
|
|
return i.v.Auth().Token().RenewTokenAsSelf(token, int((20 * time.Minute).Seconds()))
|
|
}
|