unkin-agent b9633481a5
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Make certmanager a drop-in for the Python script
Puppet calls /usr/local/bin/certmanager from generate() with combine: true, so
the CLI must take the common name positionally and keep stderr clean or the
catalog compile dies on a parsejson error.

- Flatten the CLI: drop the pki subcommand, add -a/-i/-e/-c/--json
- Emit ca_certificate so vault.pp stops writing undef to that file
- Read output_path from under vault:, where Puppet writes it
- Bound every Vault request with a timeout; exit non-zero on failure
- Gate TLS verification on vault.tls_skip_verify (default true)
- Restrict auth to approle and kubernetes, no fallback between them
- Remove the SSH half; add Woodpecker CI, nfpm RPM and completions
2026-09-19 10:34:01 +10:00
2026-03-24 19:38:24 +11:00
2026-03-24 19:38:24 +11:00

certmanager

Issues Vault PKI certificates for Puppet-managed nodes. Drop-in replacement for the Python certmanager script deployed by profiles::helpers::certmanager, compiled to a single static binary with no runtime dependencies.

Puppet calls it server-side during catalog compilation from profiles::pki::vault:

/usr/local/bin/certmanager <fqdn> --alt-names "host,fqdn" --ip-sans "127.0.0.1,<ip>" --expiry-days 90 --json

generate() runs the binary with combine: true, which splices stderr into the stdout that parsejson() reads — so on the success path the binary writes nothing but the JSON document to stdout, and nothing at all to stderr. Every failure path exits non-zero.

Usage

certmanager <common_name> [flags]
Flag Short Type Default Description
--alt-names -a comma list "" DNS subject alternative names
--ip-sans -i comma list "" IP subject alternative names
--expiry-days -e int 365 Certificate validity in days (ttl: <n>d)
--compress -c bool false Write a zip instead of a directory
--json bool false Print the certificate data as JSON
--config path /opt/certmanager/config.yaml Config file location

Comma lists are split on ,, whitespace-trimmed, and empty tokens dropped.

JSON output

{
  "certificate": "<PEM leaf>",
  "private_key": "<PEM key>",
  "full_chain": "<issuing_ca PEM>\n<certificate PEM>",
  "ca_certificate": "<issuing_ca PEM>"
}

ca_certificate is new. profiles::pki::vault writes $cert_data['ca_certificate'] to /etc/pki/tls/vault/ca_certificate.crt, but the Python never emitted the key, so Puppet saw undef and the file was never populated.

File output

Without --json the certificate is written under vault.output_path (defaulting to the working directory):

Path Contents
<output_path>/<common_name>/certificate.crt Signed certificate
<output_path>/<common_name>/private.key Private key (mode 0600)
<output_path>/<common_name>/full_chain.crt Issuing CA + certificate
<output_path>/<common_name>/ca_certificate.crt Issuing CA certificate

With -c the same four files are written into <output_path>/<common_name>.zip.

Configuration

YAML, everything nested under vault:. The path is baked in at build time (make build CONFIG_PATH=...) and overridable per-invocation with --config.

Key Required Default Notes
addr yes e.g. https://vault.service.consul:8200
auth_method no approle approle or kubernetes
mount_point yes e.g. pki_int
role_name yes e.g. servers_default
role_id iff approle
approle_path iff approle e.g. approle
k8s_mount iff kubernetes e.g. k8s/au/syd1
k8s_role iff kubernetes e.g. puppet_certmanager
jwt_path no /var/run/secrets/kubernetes.io/serviceaccount/token
output_path no . Non-JSON output directory
tls_skip_verify no true Set false once every node trusts the CA
timeout no 30s Per-request Vault timeout
vault:
  addr: https://vault.service.consul:8200
  auth_method: approle
  approle_path: approle
  role_id: <role_id>
  mount_point: pki_int
  role_name: servers_default
  output_path: /tmp/certmanager

AppRole logs in with role_id only — the Vault role sets bind_secret_id: false. Kubernetes reads the JWT from jwt_path and posts it with k8s_role. A missing, unreadable or empty JWT, a rejected login, or an unknown auth_method each fail naming the cause; there is no fallback between methods.

Development

make build        # -> dist/certmanager (CGO disabled, static)
make test         # go test -v -race ./...
make lint         # golangci-lint
make pre-commit   # uvx pre-commit run --all-files
make completions  # -> dist/completions/
make rpm          # build + package into dist/*.rpm via nfpm

The RPM installs /usr/bin/certmanager, symlinks /usr/local/bin/certmanager (the path Puppet calls), and ships bash/zsh/fish completions. On a v* tag the release pipeline builds the RPM and PUTs it to the artifactapi rpm-internal repo.

S
Description
Vault-backed X.509 certificate signing helper for Puppet
Readme 254 KiB
v0.2.0 Latest
2026-09-19 14:36:34 +10:00
Languages
Go 96.1%
Makefile 2.2%
Shell 1.7%