Skip to content

Commit c6eb767

Browse files
feat!: move the ClusterIssuer out of CreateVaultKubernetesAuth into flux (#191)
Phase 3 applied a ClusterIssuer and the TokenRequest RBAC from here, which made this step depend on the cert-manager CRDs already existing. On a fresh cluster they do not: cert-manager arrives with Flux, which runs after this. The step created the auth mount, the role, the CA Secret and the RBAC correctly and then died on a missing ClusterIssuer CRD -- everything before it green. Splitting by what each side can actually see removes the ordering problem instead of sequencing around it: here auth backend, role, CA Secret needs Vault credentials, needs no CRD flux ClusterIssuer, tokenrequest RBAC needs no credentials, dependsOn orders it The CA Secret stays because nothing else can produce it -- reading ${vaultAddr}/v1/pki/ca/pem needs a Vault token, which Flux does not have. The issuer that trusts it now lives in the flux bundle component infra/cert-manager/components/vault-issuer (flux v1.28.1), where it is also in Git for the first time: previously a rebuild recreated neither the issuer nor the Certificate depending on it. BREAKING: --issuer-name, --issuer-pki-path and --issuer-namespace are gone. --ca-secret-name keeps its meaning but now gates phase 3 on its own (default empty, so a caller that wants no cert-manager objects passes nothing), and --ca-secret-namespace replaces --issuer-namespace for the Secret's location. Callers to update: stuttgart-things pr-vm-deploy.yaml and the vault-auth.yaml the Backstage template generates. Both move the three issuer values into the cluster's platform.yaml as VAULT_ISSUER_* substitutions. Verified against cluster-test2, whose issuer was created by the old code path: the flux component adopted it by name without reissuing -- certificate serial unchanged before and after, field manager moved to kustomize-controller.
1 parent 808b9d5 commit c6eb767

1 file changed

Lines changed: 48 additions & 131 deletions

File tree

argocd/create_vault_k8s_auth.go

Lines changed: 48 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ type kubeconfigShape struct {
7474
// comma-separated list; one cluster can bind to one policy, multiple
7575
// policies, or share a policy with other clusters.
7676
//
77+
// With `--ca-secret-name` it also places Vault's PKI CA into a Secret on the
78+
// cluster. That is the only cert-manager-side object it still creates, and only
79+
// because reading `pki/ca/pem` needs a Vault token. The ClusterIssuer that
80+
// trusts that Secret, and the TokenRequest RBAC cert-manager needs to use it,
81+
// live in the flux bundle (`infra/cert-manager/components/vault-issuer`) —
82+
// applying them from here required the cert-manager CRDs to already exist, and
83+
// on a fresh cluster cert-manager arrives with Flux, i.e. after this runs.
84+
//
7785
// Idempotent: re-runs upsert the config + role and skip the auth-mount
7886
// step when the path is already in use.
7987
func (m *Argocd) CreateVaultKubernetesAuth(
@@ -124,35 +132,31 @@ func (m *Argocd) CreateVaultKubernetesAuth(
124132
// +optional
125133
// +default="3600"
126134
tokenTtl string,
127-
// Name of a cert-manager ClusterIssuer to create against this mount. Empty
128-
// creates none and the function stays what it always was: auth only.
135+
// Secret to place the Vault PKI CA into, fetched live from
136+
// ${vaultAddr}/v1/pki/ca/pem. Empty creates none and the function is auth
137+
// only.
129138
//
130-
// Set it and the cluster comes back ready to issue certificates -- the CA
131-
// Secret, the ClusterIssuer and the TokenRequest RBAC all land here, so a
132-
// caller does not have to reproduce them somewhere else.
133-
// +optional
134-
// +default=""
135-
issuerName string,
136-
// Vault sign path the issuer uses, e.g. pki/sign/4sthings.tiab.ssc.sva.de.
137-
// Required when issuer-name is set.
139+
// This is the ONLY cert-manager-side object left here, and it is here
140+
// because nothing else can produce it: reading Vault's CA needs Vault
141+
// credentials, which Flux does not have. The ClusterIssuer that consumes it
142+
// and the TokenRequest RBAC it needs both moved to the flux bundle
143+
// component infra/cert-manager/components/vault-issuer.
138144
//
139-
// Deliberately the SIGN path, not issue: cert-manager only ever uses sign,
140-
// while issue would additionally let the holder have Vault generate the
141-
// private key.
145+
// They moved because applying a ClusterIssuer from here required the
146+
// cert-manager CRDs to exist already -- and on a fresh cluster they do not,
147+
// since cert-manager itself arrives with Flux, after this step. The result
148+
// was a step that created the mount, the role and the RBAC correctly and
149+
// then died on a missing ClusterIssuer CRD. Splitting it by what
150+
// each side can actually see removes the ordering problem rather than
151+
// sequencing around it.
142152
// +optional
143153
// +default=""
144-
issuerPkiPath string,
145-
// cert-manager's cluster resource namespace -- where it looks up a
146-
// ClusterIssuer's secret references, and where the TokenRequest RBAC has to
147-
// live.
154+
caSecretName string,
155+
// Namespace for that Secret. cert-manager's cluster resource namespace,
156+
// since that is where it resolves a ClusterIssuer's secret references.
148157
// +optional
149158
// +default="cert-manager"
150-
issuerNamespace string,
151-
// Secret holding the Vault CA the issuer verifies Vault with. Fetched live
152-
// from ${vaultAddr}/v1/pki/ca/pem.
153-
// +optional
154-
// +default="vault-pki-ca"
155-
caSecretName string,
159+
caSecretNamespace string,
156160
// Cache buster — pass a timestamp/run-id from CI to force a fresh
157161
// execution. Same reason as CreateVaultIssuer: Dagger short-circuits
158162
// the whole function on input-hash match before any container in
@@ -196,13 +200,6 @@ func (m *Argocd) CreateVaultKubernetesAuth(
196200
boundNamespaces = []string{namespace}
197201
}
198202

199-
// Without a sign path the issuer would be created pointing at nothing and
200-
// would report Ready anyway -- cert-manager verifies only the login. Fail
201-
// here instead, where the cause is still visible.
202-
if issuerName != "" && issuerPkiPath == "" {
203-
return "", fmt.Errorf("issuer-pki-path is required when issuer-name is set")
204-
}
205-
206203
// Decrypt the vault env yaml (reuses the vaultEnv struct defined in
207204
// create_vault_issuer.go — same package, same shape).
208205
envYaml, err := dag.Secrets().Decrypt(ctx, sopsKey, vaultEnvFile)
@@ -284,87 +281,66 @@ func (m *Argocd) CreateVaultKubernetesAuth(
284281
return "", fmt.Errorf("configure vault k8s auth: %w", err)
285282
}
286283

287-
// Phase 3: the cert-manager side, only when asked for.
288-
if issuerName == "" {
284+
// Phase 3: the Vault CA, only when asked for.
285+
if caSecretName == "" {
289286
return applyOut + "\n" + vaultOut, nil
290287
}
291288

292-
issuerOut, err := m.vaultK8sAuthIssuer(
289+
caOut, err := m.vaultK8sAuthCaSecret(
293290
ctx,
294291
env.VaultAddr, env.VaultToken, skipVerify,
295-
clusterName, authName,
296-
issuerName, issuerPkiPath, issuerNamespace, caSecretName,
297-
boundNames[0], kubeconfigSecret,
292+
caSecretNamespace, caSecretName,
293+
kubeconfigSecret,
298294
)
299295
if err != nil {
300-
return "", fmt.Errorf("create vault clusterissuer: %w", err)
296+
return "", fmt.Errorf("place vault ca secret: %w", err)
301297
}
302-
return applyOut + "\n" + vaultOut + "\n" + issuerOut, nil
298+
return applyOut + "\n" + vaultOut + "\n" + caOut, nil
303299
}
304300

305-
// vaultK8sAuthIssuer lands everything cert-manager needs to actually issue
306-
// against the mount phase 2 created: the Vault CA in a Secret, the
307-
// ClusterIssuer, and the TokenRequest RBAC.
301+
// vaultK8sAuthCaSecret places Vault's PKI CA into a Secret on the cluster.
302+
//
303+
// This is all that is left of the cert-manager side here, and it is the only
304+
// part that could not move: reading ${vaultAddr}/v1/pki/ca/pem needs a Vault
305+
// token. The ClusterIssuer that trusts this Secret, and the TokenRequest RBAC
306+
// cert-manager needs to authenticate with it, live in the flux bundle instead.
308307
//
309308
// The CA travels as a FILE inside the container rather than through host-side
310309
// templating -- a PEM is multi-line and every layer it crosses is another place
311310
// to mangle it. `kubectl create secret --dry-run=client | kubectl apply` keeps
312311
// it idempotent without needing to know whether the Secret already exists.
313-
func (m *Argocd) vaultK8sAuthIssuer(
312+
func (m *Argocd) vaultK8sAuthCaSecret(
314313
ctx context.Context,
315314
vaultAddr, vaultToken string,
316315
skipVerify bool,
317-
clusterName, authName string,
318-
issuerName, issuerPkiPath, issuerNamespace, caSecretName string,
319-
serviceAccountName string,
316+
namespace, secretName string,
320317
kubeconfigSecret *dagger.Secret,
321318
) (string, error) {
322319
addr := strings.TrimRight(vaultAddr, "/")
323-
mountPath := fmt.Sprintf("%s-%s", clusterName, authName)
324320
curlBase := `curl -fsS`
325321
if skipVerify {
326322
curlBase += " -k"
327323
}
328324

329-
manifestVars, err := json.Marshal(map[string]string{
330-
"issuerName": issuerName,
331-
"issuerPkiPath": issuerPkiPath,
332-
"namespace": issuerNamespace,
333-
"caSecretName": caSecretName,
334-
"serviceAccount": serviceAccountName,
335-
"vaultAddr": addr,
336-
"authMountPath": "/v1/auth/" + mountPath,
337-
"authRole": authName,
338-
})
339-
if err != nil {
340-
return "", fmt.Errorf("marshal issuer manifest vars: %w", err)
341-
}
342-
manifest, err := dag.Templating().RenderInline(ctx, vaultK8sAuthIssuerTemplate,
343-
dagger.TemplatingRenderInlineOpts{Variables: string(manifestVars)})
344-
if err != nil {
345-
return "", fmt.Errorf("render issuer manifest: %w", err)
346-
}
347-
348325
script := strings.Join([]string{
349326
"set -euo pipefail",
350327
"export KUBECONFIG=/work/kubeconfig",
351328
fmt.Sprintf(`%s -H "X-Vault-Token: ${VAULT_TOKEN}" "%s/v1/pki/ca/pem" > /tmp/ca.pem`, curlBase, addr),
329+
// An empty file would produce a Secret that exists and verifies
330+
// nothing, and the issuer would still report Ready.
352331
`test -s /tmp/ca.pem`,
353-
fmt.Sprintf(`kubectl create namespace %s --dry-run=client -o yaml | kubectl apply -f -`, issuerNamespace),
332+
fmt.Sprintf(`kubectl create namespace %s --dry-run=client -o yaml | kubectl apply -f -`, namespace),
354333
fmt.Sprintf(`kubectl -n %s create secret generic %s --from-file=ca.crt=/tmp/ca.pem --dry-run=client -o yaml | kubectl apply -f -`,
355-
issuerNamespace, caSecretName),
356-
`kubectl apply -f /work/issuer.yaml`,
357-
fmt.Sprintf(`echo "clusterissuer %s -> %s/%s (sa: %s/%s)"`,
358-
issuerName, addr, issuerPkiPath, issuerNamespace, serviceAccountName),
334+
namespace, secretName),
335+
fmt.Sprintf(`echo "vault pki CA -> %s/%s (key ca.crt, $(wc -c < /tmp/ca.pem) bytes)"`, namespace, secretName),
359336
}, "\n")
360337

361-
vaultTokenSecret := dag.SetSecret("vault-k8s-auth-issuer-token", vaultToken)
338+
vaultTokenSecret := dag.SetSecret("vault-k8s-auth-ca-token", vaultToken)
362339
cacheBuster := time.Now().UTC().Format(time.RFC3339Nano)
363340

364341
ctr := dag.Container().
365342
From("alpine/k8s:1.31.0").
366343
WithMountedSecret("/work/kubeconfig", kubeconfigSecret).
367-
WithNewFile("/work/issuer.yaml", manifest).
368344
WithSecretVariable("VAULT_TOKEN", vaultTokenSecret).
369345
WithEnvVariable("CACHE_BUSTER", cacheBuster).
370346
WithExec([]string{"sh", "-c", script})
@@ -575,62 +551,3 @@ subjects:
575551
name: {{ .reviewerName }}
576552
namespace: {{ .reviewerNamespace }}
577553
`
578-
579-
// vaultK8sAuthIssuerTemplate is the cert-manager side of the mount: the
580-
// ClusterIssuer plus the TokenRequest RBAC it cannot work without.
581-
//
582-
// The RBAC is NOT optional and NOT redundant. cert-manager reaches a
583-
// Kubernetes-auth mount by minting a token for the ServiceAccount through the
584-
// TokenRequest API, which needs `create` on `serviceaccounts/token`. The chart
585-
// rendered that Role up to v1.18.x and **v1.21.1 renders no such rule at all**,
586-
// with no values flag to bring it back (verified by rendering both charts). An
587-
// upgrade across that line silently disarms every issuer of this kind -- and
588-
// silently is the word: the ClusterIssuer keeps reporting Ready, because
589-
// cert-manager verifies only the LOGIN, never the ability to sign. The only
590-
// proof is an issued Certificate.
591-
//
592-
// The CA Secret is created separately, by kubectl inside the container, so the
593-
// multi-line PEM never crosses a template boundary.
594-
const vaultK8sAuthIssuerTemplate = `apiVersion: cert-manager.io/v1
595-
kind: ClusterIssuer
596-
metadata:
597-
name: {{ .issuerName }}
598-
spec:
599-
vault:
600-
server: {{ .vaultAddr }}
601-
path: {{ .issuerPkiPath }}
602-
caBundleSecretRef:
603-
name: {{ .caSecretName }}
604-
key: ca.crt
605-
auth:
606-
kubernetes:
607-
mountPath: {{ .authMountPath }}
608-
role: {{ .authRole }}
609-
serviceAccountRef:
610-
name: {{ .serviceAccount }}
611-
---
612-
apiVersion: rbac.authorization.k8s.io/v1
613-
kind: Role
614-
metadata:
615-
name: cert-manager-tokenrequest
616-
namespace: {{ .namespace }}
617-
rules:
618-
- apiGroups: [""]
619-
resources: ["serviceaccounts/token"]
620-
resourceNames: ["{{ .serviceAccount }}"]
621-
verbs: ["create"]
622-
---
623-
apiVersion: rbac.authorization.k8s.io/v1
624-
kind: RoleBinding
625-
metadata:
626-
name: cert-manager-tokenrequest
627-
namespace: {{ .namespace }}
628-
roleRef:
629-
apiGroup: rbac.authorization.k8s.io
630-
kind: Role
631-
name: cert-manager-tokenrequest
632-
subjects:
633-
- kind: ServiceAccount
634-
name: {{ .serviceAccount }}
635-
namespace: {{ .namespace }}
636-
`

0 commit comments

Comments
 (0)