618e1414e4
Replace the nfpm script with .goreleaser.yaml as the single driver for the RPM, the raw release binaries the compiler initContainer fetches, archives and checksums. Add an end-to-end test that issues a certificate from a real Vault dev server over approle.
136 lines
5.1 KiB
Markdown
136 lines
5.1 KiB
Markdown
# 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
|
|
|
|
```json
|
|
{
|
|
"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 |
|
|
|
|
```yaml
|
|
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
|
|
|
|
```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 integration-test # end-to-end against a real Vault dev server
|
|
make release-snapshot # goreleaser: binaries, tar.gz and RPM into dist/
|
|
```
|
|
|
|
`make integration-test` starts a `hashicorp/vault` dev container with
|
|
docker/podman, provisions a PKI mount and an approle role, then runs the built
|
|
binary with the exact argv Puppet uses and asserts on the issued certificate.
|
|
Point it at an existing dev server with `CERTMANAGER_IT_VAULT_ADDR`
|
|
(`VAULT_ADDR` is deliberately ignored — the test writes to the PKI mount).
|
|
|
|
## Releasing
|
|
|
|
goreleaser drives the whole release from a `v*` tag:
|
|
|
|
- raw binaries `certmanager-<os>-<arch>` (linux/darwin, amd64/arm64) attached to
|
|
the Gitea release — the puppetserver-compiler initContainer fetches one file
|
|
by URL and checksums it, so it cannot unpack an archive;
|
|
- `tar.gz` archives and `checksums.txt`;
|
|
- `certmanager-<version>-1.<arch>.rpm`, installing `/usr/bin/certmanager`,
|
|
symlinking `/usr/local/bin/certmanager` (the path Puppet calls) and shipping
|
|
bash/zsh/fish completions.
|
|
|
|
The release pipeline then `PUT`s the RPM to the artifactapi `rpm-internal`
|
|
repo, which goreleaser cannot do itself.
|