Kubernetes / Helm
The edgepki-node Helm chart runs Mesh Nodes as a StatefulSet. Each replica gets its own persistent volume for identity and state, a stable DNS name through a headless Service, and the same hardened pod settings: non-root (UID 65532), read-only root filesystem, all capabilities dropped, RuntimeDefault seccomp, and no service-account token unless the Kubernetes CSR signer is enabled.
What the chart creates
| Object | Name (release mesh) | Notes |
|---|---|---|
| StatefulSet | mesh-edgepki-node | replicaCount replicas (default 3), container port 9443, readiness and liveness probes on GET /healthz over HTTPS |
| Service | mesh-edgepki-node | Headless (clusterIP: None), port 9443 |
| ConfigMap | mesh-edgepki-node | Generated config.json, mounted at /etc/edgepki |
| PodDisruptionBudget | mesh-edgepki-node | minAvailable: 2 by default |
| PersistentVolumeClaim | state-mesh-edgepki-node-N | ReadWriteOnce, 1Gi by default, mounted at /var/lib/edgepki |
| ServiceAccount, Lease, ClusterRole, bindings | mesh-edgepki-node-csr-signer | Only when kubernetesCsr.enabled=true |
Each pod advertises its own local API URL, https://<pod>.mesh-edgepki-node.<namespace>.svc:9443, so ACME clients and other workloads can address a specific replica.
Before you install
- Get the chart. Download
sectigo-edge-helm-charts.tgzfrom ConsoleDownloads & install (it contains theedgepki-nodeandsharppki-signercharts) and extract it. - Verify the image. The release's
container-images.jsonrecords the signed image digest. Verify it withcosign(below) and deploy by digest withimage.digest; when a digest is set, the chart ignores the tag. - Create a pull Secret for the private registry, scoped to the namespace and limited to package read access, and reference it with
imagePullSecrets. Never put registry credentials in values. - Choose a hardware key provider.
config.nodeKeyProvideris required:tpm2(expose/dev/tpmrm0through a reviewed TPM device plugin) orpkcs11(the vendor module must be in the image; put the PIN in the credential Secret underpkcs11-pin). - Create an enrollment package in ConsoleMesh nodes with the Kubernetes / Helm runtime and download the signed package plus the policy trust and registration trust files from the same dialog.
- Create the trust Secrets (below).
Verify the image digest
subject="$(jq -r .node container-images.json)"
cosign verify \
--certificate-identity-regexp '^https://github.com/hassard0/sharppki/.github/workflows/release.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
"$subject"
Use the sha256:… part of the verified subject as image.digest. Enforce the same repository, workflow, tag and issuer policy in your cluster admission controller.
Create the Secrets
The chart mounts five Secrets by default. Create them in the target namespace before the first install:
| Secret (default name) | Keys | Contents |
|---|---|---|
edgepki-node-credentials (existingSecret) | tls.crt, tls.key | Local API server certificate and key |
ca.crt | Root CA for the Sectigo Edge server connection (tls_root_ca_file) | |
client.crt, client.key | Organization-issued outbound mTLS identity | |
enrollment-token | One-time enrollment token (read from the environment, never written to the ConfigMap) | |
pkcs11-pin | Only when nodeKeyProvider=pkcs11 | |
edgepki-workload-trust | ca.crt | CA bundle for your workload trust domain only |
edgepki-policy-trust | policy-trust.json | Policy trust file from the enrollment dialog |
edgepki-registration-trust | registration-trust.json | Registration trust file from the enrollment dialog |
edgepki-acme-eab | eab-credentials.json | ACME external-account credentials. Start with an empty store: {"version":1,"credentials":[]} |
Optional Secrets: edgepki-otlp-audit (ca.pem, client.pem, client-key.pem) for audit export, edgepki-est-ca-bundle (ca-bundle.pem) for EST, and a dependency-probe Secret for active dependency discovery.
The one-time token is the enrollment_token value inside the signed package's payload. Write it to a private file and load it with --from-file, so the token never appears in a command-line argument:
kubectl create namespace edgepki
printf '%s' '{"version":1,"credentials":[]}' > eab-credentials.json
kubectl -n edgepki create secret generic edgepki-node-credentials \
--from-file=tls.crt=server.crt --from-file=tls.key=server.key \
--from-file=ca.crt=edge-server-ca.crt \
--from-file=client.crt=edge-client.crt --from-file=client.key=edge-client.key \
--from-file=enrollment-token=./enrollment-token
secret/edgepki-node-credentials created
kubectl -n edgepki create secret generic edgepki-workload-trust --from-file=ca.crt=workload-ca.crt
secret/edgepki-workload-trust created
kubectl -n edgepki create secret generic edgepki-policy-trust --from-file=policy-trust.json=policy-trust.json
secret/edgepki-policy-trust created
kubectl -n edgepki create secret generic edgepki-registration-trust --from-file=registration-trust.json=registration-trust.json
secret/edgepki-registration-trust created
kubectl -n edgepki create secret generic edgepki-acme-eab --from-file=eab-credentials.json=eab-credentials.json
secret/edgepki-acme-eab created
An enrollment token is single-use and bound to the exact node name you entered in the console. Set config.nodeName to that same name. The chart passes the credential Secret to every replica, so only one replica can consume a given token. Plan how each replica will be enrolled before scaling beyond one replica, and confirm the procedure with Sectigo support if you need several enrolled replicas from one release.
Install
tar -xzf sectigo-edge-helm-charts.tgz
helm upgrade --install mesh ./edgepki-node \
--namespace edgepki \
--set config.tenantId=acme-corp \
--set config.nodeName=k8s-prod-01 \
--set config.workloadTrustDomain=acme-corp.example \
--set 'config.allowedSpiffePrefixes={spiffe://acme-corp.example/}' \
--set 'config.allowedDnsSuffixes={.acme-corp.example}' \
--set config.nodeKeyProvider=tpm2 \
--set image.digest=sha256:4b1f0c9e7d2a6b8e3f5c1a9d0e7b2c4f6a8d1e3b5c7f9a0d2e4b6c8f1a3d5e7b \
--set 'imagePullSecrets[0].name=sectigo-edge-registry'
Release "mesh" does not exist. Installing it now.
NAME: mesh
NAMESPACE: edgepki
STATUS: deployed
REVISION: 1
Tenant, names, trust domain and digest above are examples; use the values from your enrollment package. Then watch the pods become ready:
kubectl -n edgepki get pods -l app.kubernetes.io/instance=mesh
NAME READY STATUS RESTARTS AGE
mesh-edgepki-node-0 1/1 Running 0 2m
kubectl -n edgepki logs mesh-edgepki-node-0 --tail=20
Values reference
Core (config.*)
| Value | Default | Maps to config.json |
|---|---|---|
config.tenantId | replace-me | tenant_id |
config.nodeName | kubernetes | node_name |
config.edgeUrl | https://api.sharppki.com | edge_url |
config.localApiUrl | https://sharppki-node.sharppki.svc.cluster.local:9443 | local_api_url (overridden per pod by SECTIGO_EDGE_LOCAL_API_URL) |
config.workloadTrustDomain | replace-me (required) | workload_trust_domain |
config.allowedSpiffePrefixes | ["spiffe://replace-me/"] | allowed_spiffe_prefixes |
config.trustEventSubscriberSpiffePrefixes | [] | trust_event_subscriber_spiffe_prefixes |
config.localUiEnabled | false | local_ui_enabled |
config.localUiAllowedSpiffePrefixes | [] | local_ui_allowed_spiffe_prefixes |
config.allowedDnsSuffixes | [".example.com"] | allowed_dns_suffixes |
config.maxValiditySeconds | 604800 | max_validity_seconds |
config.pollIntervalSeconds | 5 | poll_interval_seconds |
config.policyBundleMaxTtlSeconds | 300 | policy_bundle_max_ttl_seconds |
config.registrationGrantMaxTtlSeconds | 300 | registration_grant_max_ttl_seconds |
config.requirePqcTransport | true | require_pqc_transport |
config.nodeKeyProvider | "" (required) | node_key_provider (tpm2 or pkcs11) |
config.allowSoftwareNodeKey | false | allow_software_node_key |
config.pkcs11ModulePath, pkcs11TokenLabel, pkcs11TokenSerial, pkcs11KeyIdHex, pkcs11KeyLabel, pkcs11MaxSessions | empty, 8 | pkcs11_* keys |
The chart always sets listen_address to 0.0.0.0:9443, state_directory to /var/lib/edgepki, acme_external_account_required to true, cloud_fallback_enabled to true and insecure_development to false.
Image, storage and scheduling
| Value | Default | Notes |
|---|---|---|
replicaCount | 3 | One hardware identity per replica |
image.repository | ghcr.io/hassard0/sectigo-edge-node | |
image.tag | 0.1.26 | Ignored when image.digest is set |
image.digest | "" | Prefer the signed digest from container-images.json |
imagePullSecrets | [] | Namespace-scoped pull Secret |
storage.size / storage.storageClassName | 1Gi / "" | Per-replica state volume |
service.port | 9443 | |
resources | requests 50m/64Mi, limits 500m/256Mi | |
podDisruptionBudget.minAvailable | 2 | |
existingSecret, existingWorkloadTrustSecret, existingPolicyTrustSecret, existingRegistrationTrustSecret, existingAcmeEabSecret | see Secrets table | Required Secret names |
Optional features
| Value | Default | Feature |
|---|---|---|
otlpAudit.enabled, .exporterId, .endpoint, .existingSecret, .batchSize, .requestTimeoutSeconds, .pollIntervalSeconds | false, primary-siem, "", edgepki-otlp-audit, 32, 10, 5 | Signed audit export to an OTLP /v1/logs endpoint |
dependencyDiscovery.enabled, .probeIntervalSeconds, .observationTtlSeconds, .probeTimeoutSeconds, .probes, .existingSecret | false, 60, 300, 10, [], "" | Verified dependency discovery; probe files must be under /var/run/edgepki/dependency |
spire.enabled, .serverApiSocket, .parentId, .socketHostPath | false, /run/spire/server/private/api.sock, "", "" | Direct SPIRE Entry API reconciliation — see SPIFFE / SPIRE |
kubernetesCsr.* | disabled | Custom CSR signer — see Kubernetes CSR |
est.enabled, .adapterReference, .existingCaBundleSecret | false, local/est-builtin, edgepki-est-ca-bundle | EST listener — see EST |
The NGINX, Apache, Java PKCS#12, HashiCorp Vault and Microsoft ADCS adapters are not exposed as chart values.
Accessing a replica
The Service is headless and there is no Ingress. For operator access, port-forward to a specific pod:
kubectl -n edgepki port-forward pod/mesh-edgepki-node-0 9443:9443
Forwarding from 127.0.0.1:9443 -> 9443
Then use the sectigo-edge CLI against https://127.0.0.1:9443 (the local API certificate must be valid for the name you connect to, or use -url with the pod DNS name from inside the cluster).
Never expose the node through a public Ingress, and never terminate client TLS at a proxy in front of it. Workload and operator identities are client certificates that must reach the node end to end.
Upgrades and namespace considerations
- Upgrade by verifying the new image digest and running
helm upgradewith the newimage.digest. State and identity live on each replica's volume and are preserved. See Upgrades. - After the first enrollment, remove
enrollment-tokenfrom the credential Secret; an enrolled node no longer needs it. - Update ACME credentials by replacing
edgepki-acme-eabfrom a file; the node re-reads it for every new account, so no restart is needed. - Kubernetes CSR objects are cluster-scoped. If you enable the CSR signer, use a dedicated Mesh Node release and review its cluster-wide
listpermission. - Every peer pod can read a shared Secret. Where credential confidentiality between failure domains matters, deploy separately scoped releases with their own Secrets.