Replace the nfpm/build-rpm.sh release path with a single goreleaser config producing the RPM, the raw sshsignhost-<os>-<arch> binaries the puppetserver-compiler initContainer fetches, tarballs and checksums; the tag pipeline runs goreleaser then PUTs the RPM to artifactapi. Add an integration suite behind the 'integration' build tag that provisions an ssh CA mount, host role and approle on a dev-mode Vault and signs a host key through the built binary with Puppet's exact argv, asserting an empty stderr and a decoded host certificate.
2.8 KiB
AGENTS.md
Project Overview
sshsignhost signs SSH host certificates with Vault. It is a drop-in
replacement for the Python script Puppet used to render at
/opt/sshsignhost/sshsignhost, and is invoked by
profiles::ssh::sign (puppet-prod) through generate() at
/usr/local/bin/sshsignhost.
Structure
main.go # cobra command, flag surface, stdout rendering
config.go # YAML config loading, defaults, validation
vault.go # Vault HTTP client: approle/kubernetes login + sign
main_test.go # end-to-end tests against an httptest TLS stand-in for Vault
integration_test.go # -tags=integration: signs against a real dev-mode Vault
Makefile # build / test / lint / integration-test / snapshot / version bump
.goreleaser.yaml # binaries + RPM + Gitea release assets, one driver
scripts/ # integration-test.sh (dev-mode Vault in a container)
.woodpecker/ # build, test, pre-commit, integration (PR) + release (tag)
Contract with Puppet
- Flags use underscores (
--public_key,--valid_principals), matching the argv the manifest builds. --valid_principalsis forwarded to Vault as the comma-separated string it was given; it is never split and rejoined.generate()usesfailonfail: true, combine: true: stderr is duped onto stdout at spawn. On success nothing may reach stderr, orparsejson()fails and the catalog compile breaks.TestSuccessWritesNothingToStderrruns the real binary in a subprocess to hold that line.- A 200 response without
data.signed_keyis a failure, never an empty result.
Vault
- AppRole:
POST {addr}/v1/auth/{approle_path}/loginwithrole_idonly; the Vault role setsbind_secret_id: false. - Kubernetes:
POST {addr}/v1/auth/{k8s_mount}/loginwithroleand the JWT read fromjwt_path. There is no fallback between methods -- a rejected login or an unknownauth_methodfails with the cause named. - Sign:
POST {addr}/v1/{mount_point}/sign/{role_name}withX-Vault-Tokenandcert_type: host.
Known blockers
The Vault ACL policy grants sshca/sign/host, but the only role that exists is
signhost, so production signs 403 on sshca/sign/signhost even though auth
succeeds. That is a terraform-vault fix, not a bug here -- do not change the
requested path.
The signhost role sets allowed_domains: main.unkin.net,consul with
allow_bare_domains: false, so the bare hostname and the IP the manifest sends
alongside the FQDN are refused. TestIntegrationRejectsPrincipalsOutsideAllowedDomains
pins that behaviour; fixing it is another terraform-vault change.
TLS
tls_skip_verify defaults to true, preserving the Python helper's
verify=False behaviour, and is a config key so it can be flipped without a
code change once certificates are trusted everywhere.