Skip to content

fix(agent): trust a registry CA given with --ca-file (MK8S-431) - #28

Open
ezekiel-alexrod wants to merge 6 commits into
feature/MK8S-430-image-cache-commandfrom
bugfix/MK8S-431-registry-ca
Open

ezekiel-alexrod wants to merge 6 commits into
feature/MK8S-430-image-cache-commandfrom
bugfix/MK8S-431-registry-ca

Conversation

@ezekiel-alexrod

Copy link
Copy Markdown
Collaborator

Component

agent, docs

Problem

The agent can't pull from a registry signed by a private CA. The puller used go-containerregistry's default transport, so it only trusted the system CAs of the distroless image, and there was no way to hand it another one. The pull fails with x509: certificate signed by unknown authority and the node stays pending. imagecachectl goes through the same puller.Remote, so it has the same problem. That's why this branch sits on top of #25.

Fix

The manager and imagecachectl import both take --ca-file, a PEM bundle. puller.NewRemote adds it to the system pool rather than replacing it, so a registry behind a public certificate stays reachable next to the private one (agent/internal/puller/puller.go).

  • The file is read once. In the agent an unusable file (missing, or no PEM certificate in it) stops the process at startup, instead of failing every pull on every node. Rotating the CA means restarting the DaemonSet, and the docs say so. I didn't add hot reload: a CA bundle rarely changes, and a restart is cheap.
  • imagecachectl loads the CA before it reads the cache state. A bad file gets reported on every run, even one that finds the resource complete, and not only on the run that finally needs the registry. An archive source never reads it.
  • config/default/manager_registry_ca_patch.yaml mounts the ca.crt key of a registry-ca ConfigMap and points --ca-file at it. It ships commented out in config/default/kustomization.yaml, like the other optional patches there. namePrefix doesn't touch the ConfigMap name, because the ConfigMap isn't a kustomize resource.

I rejected a CA field on the ImageCache resource. It would mean a CRD change, RBAC on Secrets and one more CEL rule, for a setup with one registry per cluster. The flag follows CACertFilePath in metalk8s-registry-node-agent.

Test

  • make -C agent test, make -C agent lint and make -C agent test-e2e are green (Go 1.26.0, lint cache cleared, 2 of 2 e2e specs).
  • New puller tests run against an in-memory registry served over TLS. Without the CA they fail on x509.UnknownAuthorityError, which is the ticket's error. With it they pull. A missing file or a non-PEM file gives ErrCA and names the path. The reference uses example.com and not the loopback address, because go-containerregistry talks plain HTTP to a loopback registry and a test there would never check a certificate.
  • On kind, against registry:2 with TLS signed by a throwaway CA: without the patch the node is labelled pending and the logs show the x509 error. With the patch and the ConfigMap it's labelled synced and pause.tar is on the node. A ConfigMap holding garbage puts the pod in CrashLoopBackOff with unusable registry CA: no PEM certificate in the file: path='/etc/image-cache/registry-ca/ca.crt'.
  • imagecachectl import against the same registry exits 1 on the x509 error without --ca-file and 0 with it.
  • I didn't run the RPM tests. The RPM isn't touched.

Out of scope

  • A CA per ImageCache, registry auth, client certificates, reloading the CA without a restart.
  • Mounting the CA in the MetalK8s DaemonSet, which belongs to MK8S-166.

  • The docs describing this behaviour are updated in the same pull request:
    README.md, agent/README.md, DESIGN.md, agent/DESIGN.md,
    CONTRIBUTING.md, whichever owns it.
  • A change to the cache directory layout (subdirectory scheme, archive
    names, sentinel, permissions) lands in both halves and in
    agent/DESIGN.md. Not applicable to most pull requests.

Relates-to: MK8S-431

A registry signed by a private CA was unreachable: the puller used the
default transport, so only the image's system CAs were trusted and the
pull failed with "x509: certificate signed by unknown authority".

The manager takes a --ca-file PEM bundle and adds it to the system pool
rather than replacing it, so a registry behind a public certificate
stays reachable. The file is read once at startup, and an unusable one
stops the agent there instead of failing every pull on every node.

Relates-to: MK8S-431
The command pulls through the same puller as the agent, so it could not
reach a registry signed by a private CA either. import now takes
--ca-file too.

The CA is loaded before the cache state is read, so a file that cannot
be used is reported on every run, including one that finds the resource
already complete. An archive source reaches no registry and never reads
it.

Relates-to: MK8S-431
The agent README says what --ca-file does and how to hand the CA to the
DaemonSet, the root README covers the same flag on imagecachectl, and
the design records that the CA extends the system pool and is read once.

The kustomize patch mounts the ca.crt key of a registry-ca ConfigMap and
points --ca-file at it. It ships commented out, like the other optional
patches of config/default.

Relates-to: MK8S-431
@ezekiel-alexrod
ezekiel-alexrod requested a review from a team as a code owner September 24, 2026 17:20
@ezekiel-alexrod ezekiel-alexrod self-assigned this Sep 25, 2026
@ezekiel-alexrod ezekiel-alexrod added P2 Medium priority agent The image-cache-agent DaemonSet and its CRD labels Sep 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be difficult to integrate into MetalK8S deployment, don't know exactly how the Static-OCI-Registry CA Certificate is deployed

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The registry operator can give us this CA. It has a MirrorConfig resource for pods that pull images themselves, like this agent. When you create a MirrorConfig, the operator writes a ConfigMap with the same name, in the same namespace. That ConfigMap holds the registry CA under ca.crt.

So in MetalK8s, the integration (MK8S-166) only needs to create an empty MirrorConfig named registry-ca in the agent's namespace, then mount the ConfigMap the way the patch does. Nobody has to copy the CA by hand.

I tested it on kind with the real stack (operator 298d4d1, static-oci-registry v0.1.0-beta.2, node-agent v0.0.1-alpha.11):

  • The empty MirrorConfig becomes Ready, and the registry-ca ConfigMap contains the registry CA.
  • With the patch, the agent pulls from metalk8s-registry-server.metalk8s-registry.svc:5000 and the node becomes synced.
  • Without the patch, the node stays pending with x509: certificate signed by unknown authority, the error from the ticket.
  • imagecachectl import --ca-file also works in a pod that mounts the same ConfigMap.

I also made the comment clearer in 222683a. It now gives the ConfigMap name, the key and the namespace.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it mean that:

  • imagecache-operator is deployed in the same Namespace as Registry stack (Not surprising) ?
  • When a node first joins the cluster, the imagecachectl will be executed in a Pod ?

This solution seems to me a bit weird

Comment thread agent/internal/cli/cli.go
Comment thread agent/cmd/manager/main.go

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it be useful to add --insecure flag for testing ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea for a test cluster. Added in dd489c1 as --insecure-skip-tls-verify, like the kubectl flag. It exists on the agent and on imagecachectl import.

  • It is off by default, and no manifest in the repo turns it on.
  • The agent logs a warning at startup, and the command prints one before it pulls.
  • You can't use it with --ca-file: the two settings contradict each other, so the agent stops at startup instead of picking one.

On the same kind cluster, the agent pulls fine with it and logs the warning.

Comment thread agent/internal/cli/cli.go

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it be useful to add --insecure flag for testing ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same flag here, see my reply on main.go. The agent and the command build their registry client with the same function, puller.NewRemote.

The flag showed up under Options, but the usage line above it still
read as if --name and --cache-path were the only ones.

Relates-to: MK8S-431
The comment said to uncomment "the following line" above three lines,
and left the reader to guess that "it" was the CA and that the
ConfigMap belongs in the agent's namespace. It isn't a kustomize
resource, so neither namespace nor namePrefix is applied to it.

Relates-to: MK8S-431
Setting up a test cluster by hand means fetching the registry CA before
the agent can pull anything. The manager and imagecachectl import now
take --insecure-skip-tls-verify, which accepts any certificate.

It is off by default and no manifest sets it. The agent logs a warning
at startup and the command prints one before it pulls. It excludes
--ca-file: given both, the agent stops at startup and the command exits
2, so neither setting wins in silence.

Relates-to: MK8S-431

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it mean that:

  • imagecache-operator is deployed in the same Namespace as Registry stack (Not surprising) ?
  • When a node first joins the cluster, the imagecachectl will be executed in a Pod ?

This solution seems to me a bit weird

// newRemote builds on base; tests hand it a transport that dials their own
// server.
func newRemote(cfg TLS, base *http.Transport) (Remote, error) {
t := base.Clone()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: why cloning the given base ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent The image-cache-agent DaemonSet and its CRD P2 Medium priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants