package agent import ( "errors" "fmt" ) const ( // DefaultOutpostTokenPath is the KV-v2 path holding the Authentik API token // the agent uses to read outpost tokens. DefaultOutpostTokenPath = "service/authentik/agent-api-token" // DefaultDestKey is the KV field the outpost token is written to. DefaultDestKey = "token" ) // SeedOutpostOptions configures SeedOutpost. Every field is required; the CLI // supplies the defaults. type SeedOutpostOptions struct { VaultAddr string RoleID string AuthentikURL string Outpost string KVMount string TokenPath string DestPath string DestKey string } // SeedOutpostResult is the non-secret summary of a successful seed. type SeedOutpostResult struct { Outpost string TokenIdentifier string KVMount string DestPath string Version int } // SeedOutpost copies an Authentik outpost's token into Vault KV-v2. It reads an // Authentik API token from Vault, resolves the outpost's token identifier, // fetches the key and writes it to the destination path. The token value never // leaves this function: results and errors carry only identifiers. func SeedOutpost(o SeedOutpostOptions) (SeedOutpostResult, error) { var res SeedOutpostResult vc, err := NewVaultClient(o.VaultAddr, o.RoleID) if err != nil { return res, fmt.Errorf("vault approle login failed against %s (check VAULT_ADDR and AGENT_APPROLE_ROLE_ID): %w", o.VaultAddr, err) } secret, err := vc.ReadKV(o.KVMount, o.TokenPath) if err != nil { switch { case errors.Is(err, ErrVaultDenied): return res, fmt.Errorf("reading %s/%s denied: the agent AppRole policy does not grant read on this path (apply the terraform-vault policy change): %w", o.KVMount, o.TokenPath, err) case errors.Is(err, ErrVaultNotFound): return res, fmt.Errorf("secret %s/%s does not exist: seed the Authentik API token there first: %w", o.KVMount, o.TokenPath, err) } return res, fmt.Errorf("reading %s/%s: %w", o.KVMount, o.TokenPath, err) } apiToken := StringField(secret, "token", "api_token") if apiToken == "" { return res, fmt.Errorf("secret %s/%s has neither a 'token' nor an 'api_token' field", o.KVMount, o.TokenPath) } ac := NewAuthentikClient(o.AuthentikURL, apiToken) outpost, err := ac.FindOutpost(o.Outpost) if err != nil { if errors.Is(err, ErrOutpostNotFound) { return res, fmt.Errorf("no outpost named %q at %s: has the terraform-authentik outpost been applied?: %w", o.Outpost, o.AuthentikURL, err) } return res, fmt.Errorf("looking up outpost %q: %w", o.Outpost, err) } if outpost.TokenIdentifier == "" { return res, fmt.Errorf("outpost %q has an empty token_identifier", outpost.Name) } key, err := ac.TokenKey(outpost.TokenIdentifier) if err != nil { return res, fmt.Errorf("fetching the key for token identifier %q (the API token needs view_key on it): %w", outpost.TokenIdentifier, err) } version, err := vc.WriteKV(o.KVMount, o.DestPath, map[string]string{o.DestKey: key}) if err != nil { if errors.Is(err, ErrVaultDenied) { return res, fmt.Errorf("writing %s/%s denied: the agent AppRole policy does not grant create/update on this path (apply the terraform-vault policy change): %w", o.KVMount, o.DestPath, err) } return res, fmt.Errorf("writing %s/%s: %w", o.KVMount, o.DestPath, err) } return SeedOutpostResult{ Outpost: outpost.Name, TokenIdentifier: outpost.TokenIdentifier, KVMount: o.KVMount, DestPath: o.DestPath, Version: version, }, nil }