Skip to content

tools k8s password-vault-init

homelabctl2 tools k8s password-vault-init reads a TOML file describing groups of KV entries with per-field length / static / prevent_override metadata, generates passwords (or uses the configured static value), and writes them to a Vault KV v2 mount. It supersedes the legacy Rust homelabctl tools k8s password-vault-init and is the only command run by the in-cluster vault-credential-setup Job (k8s/manifests/base/tools/vault-credential-setup/job.yaml).

Terminal window
homelabctl2 tools k8s password-vault-init -c <toml-file> [--vault-addr URL] [--vault-token TOKEN]
FlagDefaultDescription
-c, --configrequiredPath to the TOML config (see schema below).
--vault-addr$VAULT_ADDRVault address. Required unless $VAULT_ADDR is set.
--vault-token$VAULT_TOKENVault token. Falls back to Kubernetes SA login when both arg+env are empty.

The client picks an auth method in this order:

  1. --vault-token argument
  2. $VAULT_TOKEN env var
  3. Kubernetes Service Account login at auth/<vault_k8s_mount>/login, using the projected JWT at /var/run/secrets/kubernetes.io/serviceaccount/token and the vault_role from the TOML.

The K8s path is what the in-cluster Job uses — no $VAULT_TOKEN is set, so the pod’s SA JWT is exchanged for a short-lived Vault token via the role declared in the config.

Config
├── vault_role (string, required for k8s auth)
├── vault_k8s_mount (string, default: "kubernetes")
├── kv_mount (string, default: "secrets")
├── global_path_prefix (string, optional)
└── groups: map<group-name, [GroupInstance]>
└── GroupInstance
├── path_prefix (string)
└── <entry-name>: Entry
├── type (string, "kv" required)
├── exclude_group_prefix (bool, default false)
├── exclude_global_prefix (bool, default false)
└── <field-name>: Field
├── length (int, default 20)
├── static (string, optional — overrides generated)
├── prevent_override (bool, default false)
├── special_chars (bool, default true)
└── allowed_special_chars (string, optional — narrows the special set)

Each entry is written to a Vault KV v2 path computed as:

{global_path_prefix}/{group.path_prefix}/{entry-name}

exclude_group_prefix = true drops the group’s path_prefix; exclude_global_prefix = true drops global_path_prefix. Setting both writes directly under <entry-name>.

KeyBehavior
lengthLength of the generated password (printable ASCII, uniform via crypto/rand). Default 20.
staticUse this fixed value instead of generating. length is ignored when static is set.
prevent_overrideIf true and the Vault path already has this key, the existing value is preserved (read-modify-write).
special_charsInclude non-alphanumeric characters in generated passwords. Default true. Set to false for apps that reject specials (alphanumeric-only output).
allowed_special_charsOptional string restricting which specials are used when special_chars = true. Empty (default) means all printable-ASCII specials. Ignored when special_chars = false.

prevent_override is required for credentials that downstream consumers have already issued tokens against (Authentik bootstrap, Postgres app users with active leases). Generating fresh values on every reconcile would break them.

special_chars / allowed_special_chars exist for downstream apps that either reject special characters outright or only accept a known subset (e.g. PowerDNS rejects passwords containing : in its web UI). Set special_chars = false to emit alphanumeric-only output, or set allowed_special_chars = "!@#$%" to restrict the special set.

Example: realistic vault-credential-setup.toml

Section titled “Example: realistic vault-credential-setup.toml”

This is the example config that ships in code/homelabctl/test/config.toml in the legacy repo. It seeds two Postgres accounts for the homelab-services CNPG cluster — the operator-owned acc-root is rotated on every run, and acc-vault is created once and never rewritten (Vault’s own database secrets engine then derives leases off it).

vault_role = "pg-homelab-services-setup"
global_path_prefix = "cluster/internal/ns"
kv_mount = "secret"
[[groups.cluster-pg-hl-service]]
path_prefix = "pg-homelab-services/homelab-services-cluster"
[groups.cluster-pg-hl-service.acc-root]
type = "kv"
[groups.cluster-pg-hl-service.acc-root.password]
length = 24
prevent_override = false
[groups.cluster-pg-hl-service.acc-root.username]
static = "root"
prevent_override = false
[groups.cluster-pg-hl-service.acc-vault]
type = "kv"
exclude_global_prefix = false
[groups.cluster-pg-hl-service.acc-vault.password]
length = 20
prevent_override = true
[groups.cluster-pg-hl-service.acc-vault.password2]
length = 20
[groups.cluster-pg-hl-service.acc-vault.username]
static = "vault"

That config writes two secrets:

PathKeysNotes
secret/cluster/internal/ns/pg-homelab-services/homelab-services-cluster/acc-rootusername (static root), password (24-char)Rotated on every reconcile.
secret/cluster/internal/ns/pg-homelab-services/homelab-services-cluster/acc-vaultusername (static vault), password (20-char, prevented), password2 (20-char)password is set once and preserved thereafter — anything sharing the existing lease keeps working.

The vault-credential-setup Job (k8s/manifests/base/tools/vault-credential-setup/job.yaml) mounts a ConfigMap-provided TOML at /etc/homelabctl/config/:

args:
- --log-level
- debug
- --log-format
- json
- tools
- k8s
- password-vault-init
- -c
- /etc/homelabctl/config/vault-credential-setup.toml
env:
- name: VAULT_ADDR
value: "http://openbao.openbao.svc.cluster.local:8200"

The Job runs under a SA bound to a Vault role (vault_role in the TOML) that grants write access to the configured KV mount + paths.