@@ -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.
7987func (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