Skip to main content
Version: vNext

☸️ Deploy with Helm

Published on a release tag

A release tag publishes both the image (ghcr.io/cryptos-pki/cryptos-manager, multi-arch for linux/amd64 and linux/arm64 from v0.1.0-rc.2; v0.1.0-rc.1 is linux/amd64 only) and the chart (oci://ghcr.io/cryptos-pki/charts/fleet-manager) to GHCR. Install straight from the OCI registry below, or render the chart from a checkout of the cryptos-manager repo for an unreleased commit or a local edit. Kubernetes is one of three ways to run the manager; see Other ways to run it for the other two.

The supported chart is chart/fleet-manager in the cryptos-manager repo. It ships from the same repo and the same release as the manager, so the chart and the config file it renders always match the manager they run. If you are new to the FleetOS Manager, read the FleetOS Manager overview first.

What the chart deploys​

A checkout's Chart.yaml says version 0.1.0, app version 0.1.0; the release workflow sets both to the tag without its leading v (0.1.0-rc.2 for v0.1.0-rc.2).

The chart renders four objects:

ObjectWhat it is for
ConfigMap fleet-manager-configThe manager's config.yaml, mounted at /etc/cryptos/fleet/config.yaml. It holds no secrets.
Deployment fleet-managerOne container named manager, running ghcr.io/cryptos-pki/cryptos-manager by default, or an image you built and pushed yourself.
Service fleet-managerPort 443 forwarded to 8443 in the container, and port 80 to 8080 for the redirect to HTTPS.
PersistentVolumeClaim fleet-manager-node-credsThe node credentials folder. Not created when you name your own claim in nodeCreds.existingClaim.

The pod runs as user 65532 with a read-only root filesystem, no privilege escalation and every Linux capability dropped. Its one writable mount is the node credentials claim, at /var/lib/cryptos-manager/node-creds.

The probes:

  • Readiness calls /healthz, which the manager answers itself: 200 when it is serving and its Postgres answers, 503 when the database does not. A pod whose database is down leaves the Service.
  • Startup and liveness only check that the HTTPS listener accepts connections. A database outage doesn't restart the pod, and the startup probe allows two minutes, which covers the 90 seconds the manager waits for Postgres before it listens.

What you supply​

The chart creates no secrets. Before an install you need these in the release namespace:

  • A Postgres database the cluster can reach, and a Secret holding its connection string, such as postgres://manager:<password>@db:5432/manager, under the key database-url (or the key you set in database.secretKey). Name it in database.existingSecret. The chart passes it to the manager as the MANAGER_DATABASE_URL environment variable, so the password never appears in the ConfigMap.

    To use Valkey or Redis instead of Postgres, see Storage with Valkey or Redis.

  • Storage for the node credentials. The default claim asks the cluster's default storage class for 1Gi, ReadWriteOnce.

  • The image tag. A release tag publishes ghcr.io/cryptos-pki/cryptos-manager, which image.repository already points at, but tagged with a leading v (v0.1.0-rc.2) while the chart's app version has none. Set image.tag to the release tag, or the pod can't pull its image. To track an unreleased commit or a local edit, build and push your own and set image.repository and image.tag; the manager README covers building the image yourself.

The chart refuses to render unless you set database.existingSecret or storage.mode (kv or memory), and refuses both together.

Two more are optional. Leave both out for day zero:

  • A TLS Secret with tls.crt and tls.key, the manager's server certificate, named in tls.certSecret. Without it the manager serves a self-signed certificate, kept in Postgres so every pod serves the same one, and logs its fingerprint. Add the Secret once you have a real certificate.
  • A ConfigMap with operator-ca.pem, named in operatorCA.configMap. Without it the operator CA is registered through first run, and firstRun (auto or disabled) says whether first run may open. With it, that file is the only operator CA source and first run doesn't apply.

Day zero​

Install straight from the published chart, with only the database Secret:

helm install fm oci://ghcr.io/cryptos-pki/charts/fleet-manager --version X.Y.Z --set image.tag=vX.Y.Z --set database.existingSecret=fm-postgres

Or, from a checkout, for an unreleased commit or a local edit:

helm install fm manager/chart/fleet-manager --set database.existingSecret=fm-postgres

The install notes print the commands that find the self-signed certificate's fingerprint and the bootstrap token in the log. Then follow Start first run.

The token is in the pod log

Until first run closes, anyone who can run kubectl logs on the manager can start first run. Limit who can read the pod logs in this namespace, including any log shipping, as tightly as access to the manager.

Once you have a real server certificate, set tls.certSecret in an upgrade. The pod rolls and the manager drops the stored self-signed certificate.

The node credentials claim is not replaceable

When the manager adopts a node, it writes that node's admin key to the claim, and the installed node trusts only that key. If the claim is deleted, the manager can no longer manage any node it adopted. The only way back is a reset from each node's console, which erases the node's key material. helm uninstall leaves the claim in place on purpose (helm.sh/resource-policy: keep). Back it up along with the database, and don't delete it by hand. See Re-adopting a node.

Before you upgrade​

Unpinned nodes are refused after the upgrade

Before upgrading to this version, make sure every node is either pinned (server.crt, or the fingerprint recorded at adoption) or has a recorded CA chain. Unpinned nodes are refused after the upgrade. Check node trust before you upgrade lists the nodes that would be refused and how to pin each one.

A helm upgrade that only changes values in the config still restarts the manager: the pod carries a checksum of its ConfigMap, so the new config is read straight away.

After an install or upgrade, the chart's notes print the command that lists how each node is verified, and a warning for every node with insecureSkipNodeVerify.

The values that matter​

The full list is in chart/fleet-manager/values.yaml.

KeyDefaultWhat it does
replicaCount1Number of manager pods. See the caution below before raising it.
image.repositoryghcr.io/cryptos-pki/cryptos-managerThe manager image. Pull the published image at a release tag, or build and push your own and point this at it.
image.tag""Empty means the chart's app version, which has no published image (the image tags carry a leading v): set the release tag, such as v0.1.0-rc.2.
service.port / service.targetPort443 / 8443The Service port and the container's HTTPS listener.
service.httpPort / service.httpTargetPort80 / 8080The redirect-to-HTTPS port and its listener.
httpRedirectListen0.0.0.0:8080The redirect listener. Empty turns it off.
authBypassfalseDevelopment only. Turns off client-certificate login and TLS.
tls.certSecret""The TLS Secret. Empty serves a self-signed certificate.
operatorCA.configMap""The ConfigMap with operator-ca.pem. Empty registers the operator CA through first run.
firstRunautoauto or disabled. Applies when operatorCA.configMap is empty; disabled keeps first run shut, so only an operator CA already registered in the database is trusted.
operatorCRL.urls[]http or https URLs of the operator CA's CRLs.
operatorCRL.configMap / operatorCRL.files"" / []A ConfigMap of CRL files (DER or PEM) and the keys in it to load. The chart mounts it read-only at /etc/cryptos/fleet/operator-crl, and the manager re-reads the files on every refresh. Set both or neither.
operatorRevocationPolicy""soft (the default when empty) or hard: what the web API does when no fresh revocation data is available.
operatorOCSP.mode / operatorOCSP.url"" / ""off, aia (the default when empty) or url, with url required for mode url and refused otherwise.
operatorCANode""Removed. A CryptOS node can't be the operator CA; setting it fails the render. See Migrating from operator_ca_node.
mcp.enabled / mcp.publicURLfalse / ""The MCP endpoint for AI agents. See the manager's MCP guide.
database.existingSecret""The Secret with the Postgres connection string.
database.secretKeydatabase-urlThe key inside that Secret.
storage.mode""kv or memory instead of Postgres. Can't be combined with database.existingSecret.
storage.kv.existingSecret / storage.kv.secretKey"" / kv-urlThe Secret and key with the server URL. See Storage with Valkey or Redis.
storage.kv.caConfigMap / storage.kv.caKey"" / ca.crtThe CA that signed a rediss:// server's certificate. Empty uses the system roots.
nodeCreds.existingClaim""Use this claim instead of creating one.
nodeCreds.storageClass""Empty uses the cluster default.
nodeCreds.accessModes["ReadWriteOnce"]Access modes for the created claim.
nodeCreds.size1GiSize of the created claim.
nodes[]Nodes to load into an empty database on first start.
nodes[].adminCredsSecretunsetA Secret with the node's admin credentials. See Node admin credentials from a Secret.
nodes[].insecureSkipNodeVerifyunsettrue turns off the check of the node's server certificate. Lab testing only; see Skipping verification in a lab.
The revocation values need an operator CA

operatorCRL and operatorOCSP apply to the operator CA in operatorCA.configMap, and the chart refuses to render them without it: a CA registered through first run keeps its CRL and OCSP settings on its own record. operatorRevocationPolicy applies to either source. The chart refuses to render any of them with authBypass: true, and refuses a bad value (a policy other than soft or hard, an unknown OCSP mode, a CRL URL that isn't http or https) instead of letting the manager fail at start.

More than one pod needs shared storage

Every pod must hold the admin key of every adopted node, so a ReadWriteOnce claim supports one pod only. The chart refuses to render with replicaCount above 1 unless nodeCreds.accessModes includes ReadWriteMany. With a ReadWriteOnce claim the Deployment uses the Recreate strategy, so an upgrade stops the old pod before it starts the new one and the UI is briefly unavailable.

Node admin credentials from a Secret​

A node you list in nodes needs the admin certificate and key the manager presents to it, and what the manager verifies the node against: the node's CA chain, its pinned server certificate, or both. Put them in a Secret, one per node, and name it in the node's adminCredsSecret. The chart mounts the Secret read-only at /etc/cryptos/fleet/node-admin/<name> and points the node's adminCertPath, adminKeyPath and caCertPath at it, so no key material goes in your values or the ConfigMap.

  1. Create the Secret in the release namespace, with the keys admin.crt and admin.key, plus ca.pem (the node's CA chain) once the node has its CA, and server.crt (its pinned certificate) before that. kubectl works the same on Linux, macOS and Windows:

    kubectl create secret generic pki-root-admin --from-file=admin.crt --from-file=admin.key --from-file=ca.pem --from-file=server.crt

    The manager reads ca.pem and server.crt on every connection, so an updated Secret takes effect once Kubernetes refreshes the mount, without a restart.

  2. Name it in the node's entry:

    nodes:
    - name: pki-root
    endpoint: "pki-root.example.org:443"
    role: root
    adminCredsSecret: pki-root-admin
Use the Secret or the paths, not both

The chart fails the render if a node sets adminCredsSecret together with adminCertPath, adminKeyPath or caCertPath, or has no name. A node without adminCredsSecret keeps the paths you give it.

The list only reaches an empty database

With Postgres, the manager copies nodes into the database on its first start only. A node you add to the list later does not appear. See Listed in the config file.

This is for nodes you list yourself. Nodes the manager adopts keep the admin key it makes for them on the node credentials claim.

Render the chart and check it​

You can render the chart without a cluster and read what it would create. git and helm work the same on Linux, macOS and Windows, so these commands are the same everywhere.

  1. Clone the cryptos-manager repo:

    git clone https://github.com/CryptOS-PKI/cryptos-manager.git manager
  2. Lint the chart:

    helm lint manager/chart/fleet-manager
    Expected output

    The chart passes. Lint renders it with the default values, so it notes that no store is set (you choose a database Secret or a storage.mode at install) and that it has no icon.

    level=INFO msg="funcMap fail" message="either database.existingSecret or storage.mode (kv or memory) must be set: the chart no longer falls back to an undeclared in-memory store (see docs/storage.md)"
    ==> Linting manager/chart/fleet-manager
    [INFO] Chart.yaml: icon is recommended

    1 chart(s) linted, 0 chart(s) failed
  3. Render the Deployment with the names filled in:

    helm template fm manager/chart/fleet-manager --set tls.certSecret=fm-tls --set operatorCA.configMap=fm-operator-ca --set database.existingSecret=fm-postgres --show-only templates/deployment.yaml
    Expected output

    The container gets the connection string from your Secret, not from the ConfigMap:

    env:
    - name: MANAGER_DATABASE_URL
    valueFrom:
    secretKeyRef:
    name: "fm-postgres"
    key: "database-url"
    A missing value stops the render

    Leave out both database.existingSecret and storage.mode and the render fails instead of deploying a manager with no declared store:

    Error: execution error at (fleet-manager/templates/deployment.yaml:17:4): either database.existingSecret or storage.mode (kv or memory) must be set: the chart no longer falls back to an undeclared in-memory store (see docs/storage.md)

Other ways to run it​

FleetOS Manager deploys three ways:

  • Kubernetes, above -- the published chart, or a checkout for an unreleased commit or a local edit.
  • A container (Docker or a Docker-compatible runtime) on a bare-metal host, with PostgreSQL alongside it on the same host for easy upgrades: Single host with docker compose. The compose file starts for day zero with only config.yaml and the Postgres password, plus MANAGER_VERSION set to the release tag (the compose file defaults to latest, which an rc never publishes): its tls, operator-ca and operator-crl read-only mounts are commented out until you uncomment each one with its key in config.yaml. PostgreSQL is always yours to provide -- the compose file's postgres service is a worked example you run and own, not something the manager ships or manages.
  • A plain Linux host with systemd, no container runtime, and a Postgres you provide: Deploying FleetOS Manager standalone.

Every deployment needs an operator certificate before anyone can log in. The manager's Operator credentials guide shows how to mint one.

Where to go next​