Repository navigation
feat: deliver custom CA trust without its private key - #1235
Merged
jorgemoralespou merged 113 commits intoOct 16, 2026
Merged
Conversation
ADR 0004: components receive CA trust only through an operator-owned, key-free Secret, rejecting publishing the user's Secret and adopting trust-manager. ADR 0005: the CLI keeps its local root CA on the laptop and gives clusters only per-domain, name-constrained intermediate CAs. Add the CA trust Secret, CA key pair and CA root key pair terms to the AGENTS.md glossary.
The namespaced Secret reference is not specific to CA material, so give it a general name ahead of the references that will reuse it. The JSON shape, defaults and validation are unchanged, and the generated CRDs and CLI schemas are identical.
Deploying a GKE, EKS, Inline or EducatesConfig configuration checked for an educates-custom-ca Secret in the operator namespace after the EducatesClusterConfig was Ready. Nothing creates that Secret and those configurations do not use it, so the check could block a valid deploy. Remove the check, its SkipPrereqCheck option and the prereq package. The operator already validates any CA Secret the EducatesClusterConfig references and reports it in status.
A CEL transition rule on the EducatesClusterConfig spec rejects a change of spec.ingress.certificates.provider, and the removal of spec.ingress once it is set, so a provider cannot be swapped in place or by dropping and re-adding the ingress block. Adding spec.ingress to a resource created without one is still allowed.
Operator tests used dummy bytes where a certificate belongs, which cannot exercise parsing, chain or name-constraint checks. testpki builds self-signed roots, intermediate CAs with optional name constraints and path length, and leaves with chosen SANs and validity, each returned as PEM with its key and able to emit its chain. It is a non-test package so both controller suites can import it.
Deleting a Managed EducatesClusterConfig ran the cert-manager cleanup for every certificates provider, so with provider None or StaticCertificate it uninstalled an administrator's own cert-manager release and deleted its namespace when they used the default names. The cleanup now runs only when the provider is BundledCertManager.
The session-manager and lookup-service Deployments and the node-ca-injector DaemonSet mounted the whole CA Secret. They now project only its ca.crt key through an items list, still as a directory mount so a rotated Secret propagates on the next restart. A plain Go test renders each embedded chart with the values the operator produces and asserts that every volume sourced from the CA Secret projects only ca.crt.
…ithout a wildcard The CA Secret was copied into a workshop environment namespace only as an extra rule on the wildcard TLS Secret copier, so a CA configured without a wildcard Secret never reached the namespace while workshop session pods still mounted it. The copier now carries a rule for each of the TLS and CA Secrets that is configured, and is created whenever either is.
…and session pods The training portal pod mounted the whole CA Secret, and the workshop session pod's CA volume referenced the whole Secret even though its containers used a subPath of ca.crt. Both volumes now project only the ca.crt key through an items list. The portal keeps its directory mount, so a rotated Secret propagates on the next restart.
…ocal TLS Secret When a TrainingPortal named its own TLS Secret without a different namespace, no TLS copier body was built, and appending the CA rule to it failed with an AttributeError, so portal creation kept retrying. The portal now collects its TLS and CA copy rules into one list and creates the copier whenever the list is not empty, so the CA Secret the portal pod mounts is always copied into the portal namespace.
When the API server rejects the EducatesClusterConfig apply because the deploy would change the immutable certificate provider, for example by toggling ingress.insecure on a running local cluster, the CLI now says the provider cannot change and points at 'educates admin platform delete' or recreating the cluster. The platform delete help states that cert-manager is drained only when it was bundled.
Release notes gain a Features Changed entry for the immutable certificates provider and a Bugs Fixed entry for teardown sparing a cert-manager Educates did not install. The installation and local environment guides say the provider cannot change on an existing install, how to switch, and that deletion leaves a foreign cert-manager in place.
# Conflicts: # project-docs/release-notes/version-4.0.0.md
ListCachedSecretNames only reads now: a cache directory that does not exist yet holds no secrets. Computing a deploy plan from the cache must not write to it, and creating an empty directory is a write.
admin platform render, admin platform deploy, local cluster create and local secrets sync each looked up the local CA on their own. The new localcerts package resolves the certificates of a local install once, from the local secrets cache and the config's ingress.domain and ingress.insecure, and returns a plan: the certificates decision for the translator, the cached Secrets to sync, and notices and warnings to print. Render reports the plan without committing it, so it never writes to the cache; deploy paths commit it before installing. The translator takes the decision as LocalCertificates (provider plus the CustomCA key pair reference) instead of a bare CA Secret name, and the sync pushes the Secrets the plan names. Behavior is unchanged: insecure gives provider None over HTTP, a cached CA matching the domain gives BundledCertManager with CustomCA, no match gives the same remediation error, and every cached Secret is synced.
# Conflicts: # client-programs/pkg/deployer/deploy.go
Add internal/pki, which derives the CA certificate Educates components trust from a CA trust Secret's ca.crt or from a CA key pair (its ca.crt, else the root-most certificate of its tls.crt chain), and server-side applies it as an operator-owned Opaque Secret holding only ca.crt. Only re-encoded CERTIFICATE blocks are written, so the result never carries a private key. Unusable sources report a specific condition reason.
customCA.caKeyPairSecretRef replaces caCertificateRef and names the CA key pair cert-manager signs with; the optional customCA.caCertificateSecretRef names a separate CA trust Secret. The operator checks the key pair holds tls.crt, tls.key and a CA certificate before cert-manager is involved, reporting each problem as a CertificatesReady reason. It derives the key-free educates-ingress-ca in the operator namespace, keeps it in step with its source through the Secret watch and the boot-time cache scope, and publishes it as status.ingress.caCertificateSecretRef, which also makes the node CA injector install in Auto mode.
The local translator names the cached CA key pair in educates-secrets under the renamed customCA.caKeyPairSecretRef. The embedded installer chart and the EducatesConfig schema carry the renamed field and the new optional customCA.caCertificateSecretRef.
With educates-ingress-ca published in status, the session manager, lookup service and node CA injector releases carry it with its namespace, and the node CA injector installs in Auto mode.
The kind cert-manager issuer scenario stages the CA key pair cert-manager signs with and a separate CA trust Secret holding only ca.crt, and the chart references only the latter, so the CA key stays in educates-secrets and cert-manager.
Release notes and installation guides cover the renamed customCA.caKeyPairSecretRef, the optional customCA.caCertificateSecretRef, the key-free educates-ingress-ca contract and the node CA injector installing in Auto mode on clusters with a custom CA.
…ce its references Inline mode no longer publishes the user's CA Secret. The operator derives the key-free educates-ingress-ca from its ca.crt alone, so a CA Secret that also holds a key keeps working without spreading the key. The Inline wildcardCertificateSecretRef becomes a SecretReference with an optional namespace, read through the uncached reader and published with its namespace. The Secret watch matches both Inline references in any namespace.
A CA certificate cached with its private key under a name that is neither a CA root key pair nor a CA key pair, the shape 4.0.0 alpha releases cached, now has the role invalid instead of Unused CA key pair, so local secrets list tells the user to remove it. Deploys still ignore it, sync still withholds it, and export --pem still prints its certificate so it can be registered as the root CA. The classifier reports why an entry is invalid as an InvalidCachedSecretError: LoadCachedSecret for a file that cannot be read or decoded, and InvalidCachedSecretReason for a CA key the cache does not manage.
The release notes and the local environment guide say that CAs cached by 4.0.0 alpha releases list as invalid, and register such a CA as the root CA under a new name before removing the old entry.
The command prints one cached secret as labelled lines: its role and domains as local secrets list shows them, its cache file, and for each certificate the subject, issuer, SANs, validity, CA path length, name constraints, serial and SHA-256 fingerprint. It never prints key material: only whether a private key is present, the registries of registry credentials and the key names of a generic secret. An invalid entry shows why it is invalid and the command that removes it.
The alpha CA migration test now extracts tls.key from what `educates local secrets export NAME` prints, as the documented recipe does, instead of reading the cache file. Export writes its YAML through the command's output so the test can capture it.
…ves it Removing a CA cached with its key by a 4.0.0 alpha release removes the key, so view of such an entry now says to register the CA as the root CA under another name, with the add ca command shape and the documented migration, before the remove command.
…one call ClassifyCachedSecret now returns the role together with the *InvalidCachedSecretError of an invalid Secret, replacing InvalidCachedSecretReason, and LoadClassifiedCachedSecret loads and classifies an entry, giving an unreadable file the invalid role with its reason, so list and view no longer assign that role themselves. The error names the unmanaged CA key problem in its own case.
CachedSecretRole.HoldsCertificate, beside HoldsCAKey, replaces the role lists in local secrets list and view. Export picks the root of any secret holding a CA key through HoldsCAKey.
…to secrets secrets.CachedSecretDomains and secrets.CachedSecretCertificate hold the rules, by role, for what a cached Secret is valid for and which certificate it is for, with CAKeyPairDomain as the inverse of CAKeyPairName. local secrets list and view keep only the formatting of their cells.
…s any secrets.RegistryHosts decodes only the registry names of a .dockerconfigjson for local secrets view. The Domains of an unconstrained CA and the Name constraints of a CA with none show the same unconstrainedNames value.
The printer's secret and invalid become printSecret and printInvalid, and presence, viewTime and colonHex become privateKeyField, certificateTime and certificateHex.
The list entry under Features Changed keeps its column change and points to view, which gets its own New Features entry, including how view tells you to keep a CA cached by an alpha release.
The node CA injector read only the cluster's published CA, so with a SessionManager CA override and no cluster CA, Auto skipped it and Enabled refused it, and with both, nodes trusted the cluster CA while the session manager served workshop registries with the overriding certificate. Nodes now trust the CA the session manager uses: the key-free educates-session-manager-ingress-ca when an override is set, otherwise the cluster's educates-ingress-ca. One helper, sessionManagerIngressCARef, decides it for both the session-manager and node-ca-injector values. Auto installs the injector when either CA exists; Enabled refuses only when neither does.
resolveNodeCATrust now asks effectiveIngressCARef whether a CA is in effect, so the resolver and the node-ca-injector values share one rule, and its doc comment sits on the function again. The refusal message names the SessionManager field.
Share one poller for a release's caCertificateRef value and give the override Secret helper a fixed name.
…utable CEL transition rules on the EducatesClusterConfig spec reject a change of spec.ingress.controller.provider, of spec.dns.provider, and of the policyEnforcement engines and kyverno provider, as the certificates provider already is. An absent spec.dns reads as provider None, so the block can be added or removed only with that provider. The presence of spec.policyEnforcement and of its kyverno block is frozen too, since the operator installs no Kyverno without that block. Settings within a bundled service stay mutable.
…not install on teardown Deleting a Managed EducatesClusterConfig ran the Contour, external-dns and Kyverno cleanups unconditionally, so an administrator's own install under the default release and namespace names was removed with Educates. Each cleanup now runs only when the spec has the operator install its service, which the provider immutability rules keep equal to what was installed.
When an apply of the EducatesClusterConfig is rejected because it would change how a cluster service is provided, the deploy error now names the setting that cannot change, or several together, and points at 'educates admin platform delete' or recreating the cluster. The delete help says which cluster services the teardown removes and when.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
In 4.0 alphas a custom CA never reached Educates components in Managed mode, and wherever a CA reference was published, the whole Secret, private key included, was copied into every training portal and workshop environment namespace. This PR separates CA trust from CA signing material everywhere. Components only ever receive a key-free
ca.crt, and on a laptop the root CA key never leaves the machine. The two rules are recorded indeveloper-docs/adr/0004-ca-trust-reaches-components-only-through-a-key-free-secret.mdanddeveloper-docs/adr/0005-local-ca-roots-stay-on-the-laptop.md.Where each piece lives after this PR, for the local flow with a generated root:
In this flow a CA private key exists only in
educates-secretsandcert-manager, and it is a per-domain intermediate's key, never the root's.Key-free CA trust contract (ADR 0004)
status.ingress.caCertificateSecretRefalways nameseducates-ingress-ca, an Opaque Secret the operator owns in its own namespace, holding exactly one key,ca.crt. This holds in Managed and Inline mode alike. Components never read a Secret the user created.ca.crtfrom the CA trust Secret when one is referenced, otherwise from the CA key pair'sca.crt, otherwise from the root-most certificate of itstls.crtchain, which is the rule cert-manager uses for issuedca.crt. Inline mode derives from its CA Secret'sca.crtand ignores any other key, so a CA Secret that also holds a key keeps working without the key spreading.customCA.caKeyPairSecretRef(required, what cert-manager signs with) plus an optionalcustomCA.caCertificateSecretRef(what components trust), so the issuer and the trust anchor can differ when signing from an intermediate. The key pair is checked fortls.crt,tls.keyand an IsCA certificate before cert-manager sees it, with its ownCertificatesReadyreasons.SecretReferencetype (name, optionalnamespacedefaulting to the operator namespace). Watches and the Secret cache scope follow references into foreign namespaces.SessionManageringressOverrides.caCertificateSecretRefgoes through the same derivation intoeducates-session-manager-ingress-ca, so the contract has no exception. Its namespace no longer defaults to the wildcard certificate's namespace.ca.crtthrough anitemsprojection (directory mount, so rotation still reaches the next restart). Workshop sessions already usedsubPath: ca.crt.BundleAPI is alpha and scheduled for replacement. ADR 0004 records why, and when to revisit (ClusterTrustBundle projection once the minimum Kubernetes is 1.37).SessionManageringressOverrides.tlsSecretRef, is still copied with every key it holds, since SecretCopier has no key selection. SecretCopier: copy only the keys a rule selects #1234 tracks it.StaticCertificate provider
The provider was declared in the CRD but rejected, while the docs said it worked. It now works in Managed mode: an administrator brings a wildcard certificate, the operator copies only
tls.crtandtls.keyintoeducates-wildcard-tls, and installs no cert-manager, ClusterIssuer or Certificate. One shared check serves StaticCertificate and Inline's wildcard Secret: the certificate parses, the key matches, its SANs cover*.<domain>, it has not expired, and its chain verifies against the referenced CA. Each failure has its ownCertificatesReadyreason, and the reconciler requeues at the certificate's expiry so status flips without a spec change. ExternalCertManager stays rejected.Cluster service providers are immutable
CEL transition rules make how each cluster service is provided immutable, alongside
spec.mode:spec.ingress.certificates.provider,spec.ingress.controller.provider,spec.dns.provider(an absentspec.dnsreads asNone), and thespec.policyEnforcementengines and kyverno provider, including whetherspec.policyEnforcementand itskyvernoblock are present. Switching providers in place could leave a bundled service behind or half-switch certificates, and Educates clusters are cheap to recreate; settings within a bundled service stay mutable. A consequence is that teardown now runs each cluster service's cleanup only when the spec has the operator install it, so deleting Educates no longer uninstalls a cert-manager, Contour, external-dns or Kyverno an administrator installed under the same name. The CLI turns any of these CEL rejections into a message naming the setting and pointing ateducates admin platform deleteor a new cluster.Local root and per-domain intermediates (ADR 0005)
educates local secrets add ca NAMEgenerates a root, or with--certand--keyregisters one (mkcert's root works), after checking it is a CA and the key matches. The user trusts the root once. One root per CLI data home:add caandlocal secrets importrefuse a second and name the existing one. Removing a root removes its key pair and every CA minted from it.<domain>-ca-key-pair: path length 0, a critical name constraint permitting only that domain and excluding every IPv4 and IPv6 range, valid until the root expires. Only that key pair reaches the cluster, so a key taken from a cluster cannot sign for any other site, and a new local domain needs no new trust import. Name constraints on an intermediate are enforced by browsers, OS verifiers, OpenSSL, GnuTLS, Go, Node and Java; Java would ignore them on a root.add tls NAME --cert --keycaches a wildcard certificate such as a mkcert one, after checking it is not a CA, the key matches, and it has at least one*.<domain>SAN. Domains come from certificates, never from flags.add ca NAME --certalone caches a trust-only CA certificate again, for StaticCertificate.export --pemprints certificates only. Cache files are0600in a0700directory.One resolution step in the CLI
client-programs/pkg/localcertsdecides a local deploy's certificates once, foradmin platform render,admin platform deploy,local cluster createandlocal secrets sync, so they cannot diverge. Foringress.domain:*.<domain>SAN<domain>-ca-key-pair, minted if missing or staleadd caoradd tls, unless insecureRender computes the plan without writing to the cache. Sync pushes every cached Secret except root key pairs and any CA private key other than the selected CA key pair. The stale
educates-custom-cadeploy prerequisite check is gone.local secrets listandlocal secrets viewlistprinted names only. It now printsNAME,TYPE,DOMAINSandEXPIRES.TYPEis the role a deploy sees (CA certificate,CA root key pair,CA intermediate key pair,TLS certificate,Registry credentials,Generic), orinvalidfor a file that cannot be read or a CA cached with its key by an alpha release.DOMAINSmeans "valid for": a TLS certificate's wildcard domains, a CA key pair's domain, or a CA's permitted name constraints (anywhen it has none).The new
local secrets view NAMEprints what does not fit the table: the cache file's location, the Secret type, a CA key pair's ingress domain and root, and for each certificate its common name, subject, issuer, SANs, validity, CA flag and path length, name constraints, serial and SHA-256 fingerprint. It says whether a private key is present and never prints one, shows only the hosts of registry credentials and only the key names of a generic Secret, and explains why aninvalidentry is invalid. For a CA cached by an alpha release, it says how to register that CA as the root before removing the old entry, so the CA already in the trust store is kept.Runtime fixes on the delivery path
portal.ingress.tlsCertificateRefon a cluster with a CA no longer fails creation in a retry loop.nodeCATrust.mode: Auto, the node CA injector now installs on Managed clusters with a custom CA, which includes every local HTTPS cluster, since those now publish their CA.SessionManagerCA override (ingressOverrides.caCertificateSecretRef) replaces the cluster's CA for the node CA injector too, through its key-freeeducates-session-manager-ingress-ca, and Auto installs the injector for an override even on a cluster that publishes no CA. One helper decides the CA for both the session manager's and the injector's chart values, so they cannot disagree.Where the change lives:
Breaking changes for 4.0 alpha users
4.0.0 has not shipped, so these change outright, with release notes and no deprecation cycle.
bundledCertManager.customCA.caCertificateRefis nowcaKeyPairSecretRef;staticCertificate.caCertificateRefis nowcaCertificateSecretRef. AnEducatesClusterConfigorEducatesConfigusing the old names fails validation. TheEducatesConfigschemas are regenerated with the new fields;EducatesInlineConfig, GKE and EKS kinds are unchanged.ingress.certificates.provider,ingress.controller.provider,dns.provideror thepolicyEnforcementengines and kyverno provider, or removesspec.ingress, adds or removes a non-Nonespec.dns, or adds or removesspec.policyEnforcementor itskyvernoblock, is rejected.*.<domain>, has expired, or was not issued by the named CA now leaves theEducatesClusterConfignot ready.ingressOverrides.caCertificateSecretRefdefaults to the operator namespace, not the wildcard certificate's namespace.kubernetes.io/tlsSecret with the CA and its key) are not migrated, are not used by deploy, and list asinvalid. Re-registering one as the root keeps the CA already in the trust store, then the local cluster has to be recreated, since it holds the old Secret with a changed type and the alpha CA key.--domain.add caandadd tlsno longer take it, andadd tlsrequires--certand--key.listoutput. Scripts parsing the old name-only output break; details moved to the newviewcommand.The migration commands, including the mkcert variant, are in
project-docs/release-notes/version-4.0.0.md, in the Deprecations entry that starts "The local secrets cache stores certificate authorities in a new shape".Relationship to #1232
This PR supersedes #1232's CA delivery fix and its start script
tls.crtfallback, and follows the direction @GrahamDumpleton set out in his design comment there, extended to Inline mode, StaticCertificate and the local root model.educates-ingress-cain every mode, never the user's Secretca.crtinto the combined Secret, sync backfills ittls.crtca.crtonly, and pods are projected onlyca.crt, so the fallback has nothing to read/etc/pkitrust store, and c935f87 (prune-builds)developthrough #1233. Left as is hereOnce this merges, #1232 has nothing left to land and can be closed.
Trust store direction for Debian workshop images
For @GrahamDumpleton, ahead of the Debian workshop image work. This PR does not touch workshop images or the session trust store: the
ca-trust-store-initializationinit container still runs the base environment image as root, runsupdate-ca-trust, and the workshop container mounts the regenerated/etc/pki, as #1233 left it. That design is Fedora specific and goes when workshop images move to Debian.Recommended direction. An init container runs from the workshop's own image, as non-root. It concatenates that image's
/etc/ssl/certs/ca-certificates.crtwithca.crtinto an emptyDir. The workshop container mounts that single file over/etc/ssl/certs/ca-certificates.crtand gets:SSL_CERT_FILEandREQUESTS_CA_BUNDLEpointing at the combined file;NODE_EXTRA_CA_CERTSpointing at the CA alone, since Node adds it to its own bundled roots.How each consumer is covered:
sslmodule, and Debian's GnuTLS tools such aswgetandgit, use the default bundle path.requestsusesREQUESTS_CA_BUNDLE.NODE_EXTRA_CA_CERTS, which also closes the Node gap you noted in Deliver the custom ingress CA to platform components and workshop sessions #1232.keytool, and JDK images would point theircacertsat a path the init fills. That is its own ticket.Why this over the alternatives. The two Debian equivalents of today's approach were weighed:
update-ca-certificatesand overlays all of/etc/ssl, which is Mount the whole regenerated /etc/pki into workshop sessions and widen prune-builds #1233's design on Debian;/etc/ssl/certs.Both keep the three costs of today's design. They need root in the init container, because
update-ca-certificateswrites system paths, so they either run the base image, as today, or run a workshop author's image as root, which only the platform base image does today. Running the base image means image skew: the workshop container sees a trust store built from a different image than its own, so anchors baked into a custom workshop image are lost. And overlaying a directory hides the workshop image's own files under it, the problem #1233 raised for a custom image's files under/etc/pki, such asrpm-gpg. (ii) narrows the overlay but couples the session manager to the distribution's directory layout, which on Fedora changed twice in a year.The recommended direction drops all three. Concatenating two files needs no root, so the workshop's own image can run it, the bundle starts from that image's own roots, and the overlay hides exactly one file. It is also the mechanism #1230 already uses for the runtime component images: a CA bundle file plus
SSL_CERT_FILE,REQUESTS_CA_BUNDLEandNODE_EXTRA_CA_CERTSpointing at it, so the two sides of Educates would trust the CA the same way.Known costs. Mounting a single file uses
subPath, which loses live rotation; CA rotation is out of scope, since clusters are ephemeral and a CA change means a new cluster. A tool that reads only the hashed/etc/ssl/certsdirectory, OpenSSL's CApath, would miss the CA; that is rare.Testing
Before, as observed on kind in #1232: with a Managed custom CA, no CA reached any component, the session manager and portal bundles held only public roots, and
curlto the portal URL failed withssl_verify_result=20. With #1232's class 1 applied, a Secret carryingtls.keyexisted ineducates-secrets,cert-manager,educates, the portal namespace and the workshop environment namespace.After, on kind with
educates local cluster createand a workshop session, built from this branch. Each run checks that the training portal, the workshop session and the node trust the CA, that no CA private key exists outsideeducates-secretsandcert-manager, and that no root key exists in the cluster. The wildcard certificate's own key is expected in the namespaces that serve it.Generated root, BundledCertManager with CustomCA:
Minted CA key pair "educates.test-ca-key-pair" ... from root CA "educates-dev".status.ingress.caCertificateSecretRefnameseducates-installer/educates-ingress-ca, which holds onlyca.crt, byte-equal to the exported root.cert-manager/educates-custom-caandeducates-secrets/educates.test-ca-key-pair, and the root's key nowhere.hosts.tomlis written.curlwithout-ksucceeds from the portal pod and the workshop container. On the host, it succeeds with--cacert root.pemand fails with error 60 without it.ingress.insecureand deploying again exits 1 with the immutable-provider explanation, namingeducates admin platform deleteand recreating the cluster.ingress.domaintokind.educates.testand recreating the cluster minted a new CA key pair, and the same root was trusted with nothing imported again.StaticCertificate from a mkcert wildcard, with no cert-manager installed:
cert-managernamespace or Helm release exists.CertificatesReady=Truewith reasonStaticCertificateValid, and no issuer is published.educates-wildcard-tlsholds onlytls.crtandtls.key;educates-ingress-caholds onlyca.crt, equal to the mkcert root.curlsucceed, and a pod pulls an image from the session registry.A CA without a wildcard Secret, in Inline mode with external TLS termination: the workshop environment namespace received the CA Secret holding only
ca.crt, and the session pod started and trusts the CA.The chart scenarios
02,03and08pass their checks (portal and sessioncurlwithout-k, key-free CA trust Secrets mounted withitems: [ca.crt], and scenario 08's push and pull through the session registry), but only after a Helm upgrade to this branch's images with the workshop Kyverno rules off. The runner still drives the v3 CLI, which installs an older Kyverno, and its default images predate a role rename already ondevelop. Bringing the scenarios up to date is separate work.Automated coverage added or changed in this PR:
educates-ingress-caand its singleca.crtkey,status.ingressreferences and condition reasons, StaticCertificate with no ClusterIssuer or Certificate, the CEL rejection of a provider change, expiry requeue, the SessionManager override derivation and rendered chart values, the CEL rejection of every provider change, and teardown sparing a cert-manager, Contour, external-dns or Kyverno the spec does not bundle while still removing each one it does. Atestpkihelper builds real x509 CAs, intermediates and leaves, where tests used dummy bytes before.EDUCATES_CLI_DATA_HOME: every row of the selection table, minting and re-minting, the intermediate's constraints, validity and chain, a constrained root that excludes the domain, the one-root rule, removal cascading, sync filtering, render writing nothing,listcolumns,viewoutput for every role with no Secret value ever printed, alpha CA entries listed asinvalidand the documented migration commands, and cache file modes.02-kind-tls-wildcardand03-kind-cert-manager-issuermoved to the new fields and a key-free CA trust Secret.Merge Danger
Door: two-way
4.0.0 is unreleased, so the CRD fields, status contract and cache shape can still change, and reverting the PR restores the previous behaviour. A revert would strand caches users have already re-registered in the new shape, which they would have to re-add by hand.
Blast Radius: installer-wide
EducatesClusterConfig/EducatesConfigusing the old field names must migrate before their next deploy.nodeCATrust.mode: Disabledis set.