feat: support Vault as TLS automation cert issuer - #440
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## feat/ct/tls-static-dynamic #440 +/- ##
==============================================================
+ Coverage 93.93% 94.29% +0.36%
==============================================================
Files 46 46
Lines 2918 2999 +81
==============================================================
+ Hits 2741 2828 +87
+ Misses 176 170 -6
Partials 1 1
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
e1a31e6 to
152ba43
Compare
There was a problem hiding this comment.
Pull request overview
Adds Vault PKI support for dynamically issued downstream TLS certificates and reuses shared Vault authentication across TLS and SSH.
Changes:
- Adds Vault-backed dynamic TLS issuance and configuration.
- Refactors Vault authentication into a shared package.
- Adds downstream TLS support for Web App resources and local tooling.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
.golangci.yml |
Allows the issuer interface name. |
internal/config/config.go |
Adds Vault TLS CA configuration. |
internal/config/config_test.go |
Tests shared Vault and TLS configuration. |
internal/connect/cert.go |
Implements Vault PKI certificate issuance. |
internal/connect/cert_provider.go |
Propagates handshake contexts. |
internal/connect/cert_provider_test.go |
Tests Vault provider creation. |
internal/connect/cert_reloader.go |
Adopts the context-aware interface. |
internal/connect/cert_reloader_test.go |
Updates reloader tests. |
internal/connect/cert_test.go |
Tests dynamic and Vault issuers. |
internal/connect/conn.go |
Passes handshake context during issuance. |
internal/sshhandler/ca.go |
Uses the shared Vault client. |
internal/sshhandler/proxy_test.go |
Updates shared Vault test setup. |
internal/token/gat_claims.go |
Adds Web App downstream TLS metadata. |
internal/token/gat_claims_test.go |
Tests downstream TLS claims. |
internal/vault/vault.go |
Provides shared Vault authentication and renewal. |
internal/vault/vault_test.go |
Tests shared Vault behavior. |
test/fake/client.go |
Supports TLS-enabled Web App tokens. |
test/integration/ssh_test.go |
Migrates SSH Vault integration configuration. |
tools/local/main.go |
Adds a local HTTPS Web App client. |
Suppressed comments (1)
internal/connect/cert.go:282
- This omits the requested host from
alt_namesand sends IP literals as DNS SANs. If the Vault role setsexclude_cn_from_sans, the primary hostname is not covered; IP resources also requireip_sans, so their handshakes fail hostname verification. Split allnamesby IP/hostname, include the primary hostname inalt_names, and send IPs throughip_sansas described in the PR.
if len(names) > 1 {
data["alt_names"] = strings.Join(names[1:], ",")
}
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
98469da to
b7273ef
Compare
63c44a0 to
b56499c
Compare
|
This pull request is stale because it has been open 5 days with no activity. Remove stale label or comment or this will be closed in 5 days. |
6e9d18b to
7486aad
Compare
- add tls.automation.issuer.vault, issuing leaf certificates through Vault's PKI secrets engine alongside the existing local CA issuer - share the Vault client, auth and token-renewal code between the TLS and SSH CAs in a new internal/vault package - issue DNS SANs only; the no-SNI local-address fallback still serves probes, which skip verification Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Health probes dial the gateway by IP without SNI, so the local-address fallback requests a certificate for a bare IP. Split IP aliases into ip_sans when issuing through Vault so those certificates verify, matching the self-sign issuer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
b56499c to
8015a01
Compare
| Address string `yaml:"address"` // Vault server address, e.g. https://vault.example.com:8200 | ||
| CABundleFile string `yaml:"caBundleFile,omitempty"` // Path to a PEM CA bundle for verifying Vault's TLS certificate; omit to use the system trust store | ||
| Auth SSHCAVaultAuthConfig `yaml:"auth"` | ||
|
|
||
| Namespace string `yaml:"namespace,omitempty"` // Optional Vault namespace |
There was a problem hiding this comment.
These are moved to VaultConfig
| ttl: "5m" | ||
| ca: | ||
| vault: | ||
| server: "https://vault:8200" |
There was a problem hiding this comment.
Should be address typo from previous commit
2f39806 to
af8c627
Compare
Port the Vault issuer onto the `sign` seam the base branch introduced: `vaultIssuer` now implements `sign` and reuses the shared `issueCertificate`, so CSR building, chain assembly and single-name issuance live in one place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The base branch dropped the no-SNI local-address fallback, so a handshake without SNI now asks Vault to sign a request with no names at all, which a PKI role only accepts with require_cn=false. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reseat the Vault issuer on the DER-based `sign` seam the base branch settled on: `vaultIssuer` no longer holds a key config or issues certificates itself, it just parses the request and forwards it to Vault. Leaf key generation, CSR building and chain assembly now live in `automation` for both backends. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Re-point the Vault backend at the `certificateRequest` struct the base branch introduced: `sign` takes the request instead of DER, so `vaultIssuer` asks it for a CSR only when it needs one to forward, reads the names off its fields, and no longer carries its own `ttl`. `commonName` and `verifyIssuedCertificate` read the struct too, which leaves no `x509.ParseCertificateRequest` in the package. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| tests := []struct { | ||
| name string | ||
| authSetup func(t *testing.T) gatewayconfig.VaultAuthConfig | ||
| }{ | ||
| { | ||
| name: "token", | ||
| authSetup: func(t *testing.T) gatewayconfig.VaultAuthConfig { | ||
| t.Helper() | ||
|
|
||
| return gatewayconfig.VaultAuthConfig{ | ||
| Token: testutil.SetupVaultToken(t, vaultContainerID), | ||
| } | ||
| }, | ||
| }, | ||
| { | ||
| name: "approle", | ||
| authSetup: func(t *testing.T) gatewayconfig.VaultAuthConfig { | ||
| t.Helper() | ||
|
|
||
| roleID, secretID := testutil.SetupVaultAppRole(t, vaultContainerID) | ||
|
|
||
| return gatewayconfig.VaultAuthConfig{ | ||
| AppRole: &gatewayconfig.VaultAppRoleConfig{ | ||
| RoleID: roleID, | ||
| SecretID: secretID, | ||
| }, | ||
| } | ||
| }, | ||
| }, | ||
| } |
There was a problem hiding this comment.
We can do it later but I think it's better to have a dedicate test for Vault auth. When we support a new auth method, we shouldn't have to add them to both this and SSH test.
# Conflicts: # internal/connect/cert/automation_test.go
| // pem_bundle returns the CA chain concatenated onto the leaf | ||
| "format": "pem_bundle", |
There was a problem hiding this comment.
I think it'd be simpler to use pem_bundle to let Vault return the certificate chains. So we avoid constructing the chain ourselves with vaultCAChainPEMs()
Related Tickets
Issue: #423
Changes
tls.automation.issuer.vaultconfig. When configured, Gateway can issue a TLS cert via Vault through the/signendpoint.SSHCAVault*→Vault*) for reuse by both SSH and TLS configs; the YAML config schema is unchangedVAULT_TOKENfallback and automatic token renewal