Add agentvault with a seed-outpost subcommand
Interactive agents are classifier-blocked from plumbing credentials through a shell, so seeding an Authentik outpost token into Vault KV needs to happen inside one binary invocation that never exposes the secret. - Add cmd/agentvault, a fourth CLI sharing the agentpr Vault AppRole login (role_id only, VAULT_ADDR/AGENT_APPROLE_ROLE_ID defaults unchanged). - Add `agentvault seed-outpost`: read the Authentik API token from kv/service/authentik/agent-api-token (field `token`, falling back to `api_token`), exact-match the outpost by name via the instances search, fetch its key from /api/v3/core/tokens/<identifier>/view_key/ and write it to --dest-path under --dest-key. - Print only the outpost name, token identifier, dest path and new KV version; keep secret material out of results, errors and logs. - Distinguish the failure stages (login, KV read denied, outpost missing, view_key, KV write denied) with ErrVaultDenied/ErrVaultNotFound/ ErrOutpostNotFound sentinels and actionable messages. - Add internal/agent vaultkv.go (AppRole-authenticated KV-v2 client) and authentik.go (outpost search + view_key) for reuse by future flows. - Cover the happy path, idempotent re-run, field fallback and every failure mode with httptest servers, including a leak check on error strings. - Wire agentvault into the Makefile, build-rpm.sh, nfpm contents, release cross-builds/assets, README and AGENTS.md.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
// Command agentvault runs deterministic Vault flows for agents in a single
|
||||
// invocation, so credentials are never plumbed through a shell. It authenticates
|
||||
// with the same Vault AppRole as agentpr (role_id only, no secret_id).
|
||||
//
|
||||
// agentvault seed-outpost --outpost <name> --dest-path <kv/path>
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.unkin.net/unkin/agent-tools/internal/agent"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
if err := newRootCmd().Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// newRootCmd builds the agentvault command tree. Separated from main so tests
|
||||
// can execute it against httptest servers.
|
||||
func newRootCmd() *cobra.Command {
|
||||
root := &cobra.Command{
|
||||
Use: "agentvault",
|
||||
Short: "Run deterministic Vault flows as the agent AppRole.",
|
||||
Long: "agentvault performs self-contained Vault flows for agents: it logs in with the\nagent AppRole and moves secret material between systems without ever printing it.",
|
||||
Version: version,
|
||||
SilenceUsage: true,
|
||||
}
|
||||
root.SetVersionTemplate("{{.Version}}\n")
|
||||
root.AddCommand(newSeedOutpostCmd(), newVersionCmd())
|
||||
return root
|
||||
}
|
||||
|
||||
func newSeedOutpostCmd() *cobra.Command {
|
||||
opts := agent.SeedOutpostOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "seed-outpost",
|
||||
Short: "Copy an Authentik outpost token into Vault KV",
|
||||
Long: "Read the Authentik API token from Vault KV, resolve the named outpost's\n" +
|
||||
"token_identifier, fetch its key and write it to a Vault KV path. Re-running\n" +
|
||||
"writes a new KV version. The token value is never printed or logged.",
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.VaultAddr = agent.VaultAddr()
|
||||
opts.RoleID = agent.RoleID()
|
||||
res, err := agent.SeedOutpost(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out := cmd.OutOrStdout()
|
||||
_, _ = fmt.Fprintf(out, "outpost: %s\n", res.Outpost)
|
||||
_, _ = fmt.Fprintf(out, "token_identifier: %s\n", res.TokenIdentifier)
|
||||
_, _ = fmt.Fprintf(out, "dest: %s/%s\n", res.KVMount, res.DestPath)
|
||||
_, _ = fmt.Fprintf(out, "version: %d\n", res.Version)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
f := cmd.Flags()
|
||||
f.StringVar(&opts.Outpost, "outpost", "", "Authentik outpost name (required)")
|
||||
f.StringVar(&opts.DestPath, "dest-path", "", "KV-v2 path to write the token to, e.g. kubernetes/namespace/authentik/default/outpost-token (required)")
|
||||
f.StringVar(&opts.DestKey, "dest-key", agent.DefaultDestKey, "Field to write the token under")
|
||||
f.StringVar(&opts.KVMount, "kv-mount", agent.DefaultKVMount, "KV-v2 mount holding both the API token and the destination")
|
||||
f.StringVar(&opts.TokenPath, "token-path", agent.DefaultOutpostTokenPath, "KV-v2 path of the Authentik API token")
|
||||
f.StringVar(&opts.AuthentikURL, "authentik-url", agent.AuthentikURL(), "Authentik base URL")
|
||||
_ = cmd.MarkFlagRequired("outpost")
|
||||
_ = cmd.MarkFlagRequired("dest-path")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newVersionCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the version",
|
||||
Run: func(cmd *cobra.Command, args []string) { fmt.Println(version) },
|
||||
SilenceUsage: true,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user