Skip to content

feat: deliver custom CA trust without its private key - #1235

Merged
jorgemoralespou merged 113 commits into
educates:developfrom
jorgemoralespou:managed-ca-fixes
Oct 16, 2026
Merged

jorgemoralespou merged 113 commits into
educates:developfrom
jorgemoralespou:managed-ca-fixes

Conversation

@jorgemoralespou

Copy link
Copy Markdown
Collaborator

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 in developer-docs/adr/0004-ca-trust-reaches-components-only-through-a-key-free-secret.md and developer-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:

laptop (CLI data home)
├── NAME                    CA trust Secret, ca.crt = root            synced
├── NAME-root-key-pair      CA root key pair                          never synced
└── <domain>-ca-key-pair    CA key pair: intermediate + key,          synced only when
                            ca.crt = root                             the deploy selects it

cluster
├── educates-secrets/<domain>-ca-key-pair        customCA.caKeyPairSecretRef
│   └── cert-manager/educates-custom-ca          ClusterIssuer signs educates-wildcard-tls
└── educates-secrets/NAME                        customCA.caCertificateSecretRef
    └── educates-installer/educates-ingress-ca   ca.crt only = status.ingress.caCertificateSecretRef
        └── educates, portal and workshop environment namespaces (SecretCopier)
            └── pods mount items: [ca.crt]

In this flow a CA private key exists only in educates-secrets and cert-manager, and it is a per-domain intermediate's key, never the root's.

Key-free CA trust contract (ADR 0004)

  • status.ingress.caCertificateSecretRef always names educates-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.
  • The operator derives ca.crt from the CA trust Secret when one is referenced, otherwise from the CA key pair's ca.crt, otherwise from the root-most certificate of its tls.crt chain, which is the rule cert-manager uses for issued ca.crt. Inline mode derives from its CA Secret's ca.crt and ignores any other key, so a CA Secret that also holds a key keeps working without the key spreading.
  • Managed mode's custom CA is now customCA.caKeyPairSecretRef (required, what cert-manager signs with) plus an optional customCA.caCertificateSecretRef (what components trust), so the issuer and the trust anchor can differ when signing from an intermediate. The key pair is checked for tls.crt, tls.key and an IsCA certificate before cert-manager sees it, with its own CertificatesReady reasons.
  • Every Secret reference uses one SecretReference type (name, optional namespace defaulting to the operator namespace). Watches and the Secret cache scope follow references into foreign namespaces.
  • A SessionManager ingressOverrides.caCertificateSecretRef goes through the same derivation into educates-session-manager-ingress-ca, so the contract has no exception. Its namespace no longer defaults to the wildcard certificate's namespace.
  • The session manager, lookup service and node CA injector charts, and the training portal pod, mount only ca.crt through an items projection (directory mount, so rotation still reaches the next restart). Workshop sessions already used subPath: ca.crt.
  • trust-manager is not adopted. It solves distribution, which the SecretCopier already does, not the pod trust store, and its Bundle API is alpha and scheduled for replacement. ADR 0004 records why, and when to revisit (ClusterTrustBundle projection once the minimum Kubernetes is 1.37).
  • Not covered here: an Inline wildcard Secret, or a SessionManager ingressOverrides.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.crt and tls.key into educates-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 own CertificatesReady reason, 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 absent spec.dns reads as None), and the spec.policyEnforcement engines and kyverno provider, including whether spec.policyEnforcement and its kyverno block 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 at educates admin platform delete or a new cluster.

Local root and per-domain intermediates (ADR 0005)

  • educates local secrets add ca NAME generates a root, or with --cert and --key registers 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 ca and local secrets import refuse a second and name the existing one. Removing a root removes its key pair and every CA minted from it.
  • The first deploy for an ingress domain mints <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.
  • A stale CA key pair (expired, or minted from a replaced root) is minted again. A supplied root with its own name constraints must permit the domain.
  • add tls NAME --cert --key caches 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 --cert alone caches a trust-only CA certificate again, for StaticCertificate.
  • export --pem prints certificates only. Cache files are 0600 in a 0700 directory.

One resolution step in the CLI

client-programs/pkg/localcerts decides a local deploy's certificates once, for admin platform render, admin platform deploy, local cluster create and local secrets sync, so they cannot diverge. For ingress.domain:

Cached Result
Exactly one unexpired TLS certificate with a *.<domain> SAN StaticCertificate, trusting the cached CA certificate its chain verifies against (warning when none does), with a notice that cert-manager will not be installed, which certificate to remove to get it, and the other domains it covers
More than one unexpired TLS certificate for the domain Error naming them
No TLS certificate for the domain, one root BundledCertManager with CustomCA through <domain>-ca-key-pair, minted if missing or stale
No TLS certificate for the domain, no root Error suggesting add ca or add tls, unless insecure

Render 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-ca deploy prerequisite check is gone.

local secrets list and local secrets view

list printed names only. It now prints NAME, TYPE, DOMAINS and EXPIRES. TYPE is the role a deploy sees (CA certificate, CA root key pair, CA intermediate key pair, TLS certificate, Registry credentials, Generic), or invalid for a file that cannot be read or a CA cached with its key by an alpha release. DOMAINS means "valid for": a TLS certificate's wildcard domains, a CA key pair's domain, or a CA's permitted name constraints (any when it has none).

The new local secrets view NAME prints 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 an invalid entry 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

  • The session manager copies the CA Secret into workshop environment namespaces whenever a CA is configured, not only together with a wildcard Secret. Before, a CA-only configuration produced session pods mounting a Secret that was never copied.
  • A training portal naming its own portal.ingress.tlsCertificateRef on a cluster with a CA no longer fails creation in a retry loop.
  • With 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.
  • Nodes trust the CA the session manager uses. A SessionManager CA override (ingressOverrides.caCertificateSecretRef) replaces the cluster's CA for the node CA injector too, through its key-free educates-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:

installer/operator/
├── api/config/v1alpha1/                    SecretReference, renamed fields, CEL rule
├── internal/pki/                           key-free derivation, wildcard certificate check
└── internal/controller/
    ├── config/ingressca.go                 educates-ingress-ca
    ├── config/wildcardcertificate.go       StaticCertificate and Inline validation
    └── platform/sessionmanager_ingressca.go
client-programs/pkg/
├── localcerts/                             the resolution step
└── secrets/{roles,pki}.go                  cache roles, minting, x509 checks
installer/charts/.../charts/                items: [ca.crt] mounts
session-manager/handlers/                   CA copy guard, portal mount

Breaking changes for 4.0 alpha users

4.0.0 has not shipped, so these change outright, with release notes and no deprecation cycle.

  • Renamed fields. bundledCertManager.customCA.caCertificateRef is now caKeyPairSecretRef; staticCertificate.caCertificateRef is now caCertificateSecretRef. An EducatesClusterConfig or EducatesConfig using the old names fails validation. The EducatesConfig schemas are regenerated with the new fields; EducatesInlineConfig, GKE and EKS kinds are unchanged.
  • Immutable providers. An update that changes ingress.certificates.provider, ingress.controller.provider, dns.provider or the policyEnforcement engines and kyverno provider, or removes spec.ingress, adds or removes a non-None spec.dns, or adds or removes spec.policyEnforcement or its kyverno block, is rejected.
  • Stricter Inline validation. A wildcard certificate that does not cover *.<domain>, has expired, or was not issued by the named CA now leaves the EducatesClusterConfig not ready.
  • Override namespace. ingressOverrides.caCertificateSecretRef defaults to the operator namespace, not the wildcard certificate's namespace.
  • Cache shape. CAs cached by alpha releases (one kubernetes.io/tls Secret with the CA and its key) are not migrated, are not used by deploy, and list as invalid. 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.
  • Removed --domain. add ca and add tls no longer take it, and add tls requires --cert and --key.
  • New list output. Scripts parsing the old name-only output break; details moved to the new view command.

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.crt fallback, and follows the direction @GrahamDumpleton set out in his design comment there, extended to Inline mode, StaticCertificate and the local root model.

#1232 Here
615bf14, class 1a: publish the custom CA ref in Managed mode Replaced. The operator publishes the derived, key-free educates-ingress-ca in every mode, never the user's Secret
615bf14, class 1b: CLI writes ca.crt into the combined Secret, sync backfills it Replaced by the split cache: CA trust Secret, root key pair kept local, per-domain CA key pair
abbbb90, class 2: start scripts fall back to tls.crt Not adopted. Start scripts read ca.crt only, and pods are projected only ca.crt, so the fallback has nothing to read
9aae3fc, class 3: Fedora 44 /etc/pki trust store, and c935f87 (prune-builds) Already merged to develop through #1233. Left as is here

Once 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-initialization init container still runs the base environment image as root, runs update-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.crt with ca.crt into an emptyDir. The workshop container mounts that single file over /etc/ssl/certs/ca-certificates.crt and gets:

  • SSL_CERT_FILE and REQUESTS_CA_BUNDLE pointing at the combined file;
  • NODE_EXTRA_CA_CERTS pointing at the CA alone, since Node adds it to its own bundled roots.

How each consumer is covered:

  • Go reads that bundle file first.
  • Python's ssl module, and Debian's GnuTLS tools such as wget and git, use the default bundle path.
  • Python requests uses REQUESTS_CA_BUNDLE.
  • Node uses 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.
  • Java still needs a generated truststore: the init container would run the JDK image's own keytool, and JDK images would point their cacerts at 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:

Both keep the three costs of today's design. They need root in the init container, because update-ca-certificates writes 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 as rpm-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_BUNDLE and NODE_EXTRA_CA_CERTS pointing 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/certs directory, 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 curl to the portal URL failed with ssl_verify_result=20. With #1232's class 1 applied, a Secret carrying tls.key existed in educates-secrets, cert-manager, educates, the portal namespace and the workshop environment namespace.

After, on kind with educates local cluster create and 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 outside educates-secrets and cert-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:

  • Render left the CLI data home byte-identical; deploy printed Minted CA key pair "educates.test-ca-key-pair" ... from root CA "educates-dev".
  • status.ingress.caCertificateSecretRef names educates-installer/educates-ingress-ca, which holds only ca.crt, byte-equal to the exported root.
  • A scan of every Secret, ConfigMap and Helm release found the intermediate's key only in cert-manager/educates-custom-ca and educates-secrets/educates.test-ca-key-pair, and the root's key nowhere.
  • The node CA injector runs in Auto mode, the node's CA file equals the root, and a per-registry hosts.toml is written.
  • curl without -k succeeds from the portal pod and the workshop container. On the host, it succeeds with --cacert root.pem and fails with error 60 without it.
  • An image pushed to the session registry through the ingress is pulled by a pod that starts.
  • Toggling ingress.insecure and deploying again exits 1 with the immutable-provider explanation, naming educates admin platform delete and recreating the cluster.
  • Switching ingress.domain to kind.educates.test and 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:

  • Render and create print the StaticCertificate notice; render wrote nothing.
  • No cert-manager namespace or Helm release exists. CertificatesReady=True with reason StaticCertificateValid, and no issuer is published.
  • educates-wildcard-tls holds only tls.crt and tls.key; educates-ingress-ca holds only ca.crt, equal to the mkcert root.
  • Portal, session and host curl succeed, 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, 03 and 08 pass their checks (portal and session curl without -k, key-free CA trust Secrets mounted with items: [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 on develop. Bringing the scenarios up to date is separate work.

Automated coverage added or changed in this PR:

  • Operator envtest, both suites: each derivation rule for educates-ingress-ca and its single ca.crt key, status.ingress references 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. A testpki helper builds real x509 CAs, intermediates and leaves, where tests used dummy bytes before.
  • CLI, against a temporary 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, list columns, view output for every role with no Secret value ever printed, alpha CA entries listed as invalid and the documented migration commands, and cache file modes.
  • Chart scenarios 02-kind-tls-wildcard and 03-kind-cert-manager-issuer moved 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

  • Anyone on a 4.0 alpha with a local cache or a GitOps EducatesClusterConfig / EducatesConfig using the old field names must migrate before their next deploy.
  • Every local HTTPS cluster now runs the privileged node CA injector DaemonSet unless nodeCATrust.mode: Disabled is set.
  • Inline installs with a wildcard certificate that does not match the domain or the named CA, which used to come up, now stay not ready.
  • The runtime changes in the session manager handlers and charts have no unit tests; the kind runs above are their acceptance.

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.
@jorgemoralespou
jorgemoralespou merged commit 5e6f5e4 into educates:develop Oct 16, 2026
2 checks passed
@jorgemoralespou
jorgemoralespou deleted the managed-ca-fixes branch October 16, 2026 07:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant