# 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 --alt-names "host,fqdn" --ip-sans "127.0.0.1," --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 [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: 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 ```json { "certificate": "", "private_key": "", "full_chain": "\n", "ca_certificate": "" } ``` `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 | |---|---| | `//certificate.crt` | Signed certificate | | `//private.key` | Private key (mode 0600) | | `//full_chain.crt` | Issuing CA + certificate | | `//ca_certificate.crt` | Issuing CA certificate | With `-c` the same four files are written into `/.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 | ```yaml vault: addr: https://vault.service.consul:8200 auth_method: approle approle_path: approle 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 ```sh 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 `PUT`s it to the artifactapi `rpm-internal` repo.