Utila signs a DALP transaction only after a co-signer you run approves it. The Helm chart can run that co-signer, called the signing agent here, beside DALP. In its default mode the agent asks DALP before it signs, and DALP answers yes only when the held request matches the request DALP filed. The agent adds no judgement of its own: it catches a request changed after DALP filed it, not a request DALP should not have filed, so approval by a person belongs in your Utila policy. The agent is off by default. Before you enable it, you need a Utila provider configuration, a device id from registration, an image you built and a state store.
Use this page to render the workload and to set its values. For enrolment, key rotation and incident work, use the Utila signing agent runbook.
The agent calls DALP over the cluster network, and the public edge never routes the decision route, so outside requests cannot reach it.
What the chart renders
The utilaSigningAgent block in values.yaml is off by default. When you enable it, the chart renders these objects in the release namespace:
The chart renders no Service for the agent, and no Ingress, HTTPRoute or Route. The agent polls Utila and calls DALP, and nothing calls the agent.
Build the image
The chart never builds or pulls a public image. Build the image yourself from Utila's distroless Dockerfile with the Utila CLI binary from the Utila Resource Centre, then push it to a registry your cluster trusts and record the digest.
Utila's Dockerfile copies the CLI binary into a distroless non-root image and sets it as the entrypoint. The chart passes cosigner run as arguments to that entrypoint. The Utila co-signer guide asks for CLI version 1.5.0 or later.
Set both image.repository and image.digest. The chart refuses to render without them. An upgrade changes the digest. The new pod starts against the existing state and does not enrol again.
Set the values
These values are required when utila is true:
dapiInternalUrl defaults to http://dalp-api:3001, the Service of the API pods for a release named dalp. Change it when your release has another name or your API Service sits on another port. It must not be empty in webhook-decision mode.
Default invocation
The default arguments run the co-signer in webhook-decision mode. The chart adds the vault id, the device id and the decision address to the arguments cosigner run, so the pod runs the equivalent of:
utila cosigner run --vault-id <vault-id> --device-id <device-id> \
--webhook-url <dapiInternalUrl>/internal/custody/utila/signing-decision/<organization-id>/<provider-configuration-id>/$(SIGNING_AGENT_SECRET)The agent secret is the last segment of the address. Kubernetes expands $(SIGNING_AGENT_SECRET) from a Secret reference, so the secret never sits in values or in a ConfigMap. Anyone who can read the pod's process arguments can read the expanded secret. Restrict pods/exec and pod read access in the namespace, and never log the address. The Platform API records no request span for any request on the decision address, however the request spells it, in upper case, with doubled slashes or with percent-encoding, so the address and the secret in it do not reach the trace store.
The command and flags follow the Utila co-signer setup guide, which shows utila cosigner run --. Utila's Dockerfile names its binary utilacli in the build context. Check the flags against the CLI version you build, and use command, args and extraArgs to adapt the invocation.
Sign-all mode
mode: sign- runs the co-signer with --sign-all-txs. It signs every transaction designated to the service account without asking DALP. That makes the agent a signing oracle for the whole service account. Use it for first-contact testing in a vault with no value, then set mode back to webhook-decision. The chart adds a pod annotation and a warning to the Helm notes while it is set.
State and keys
The agent keeps its device key and key shares in a state store, encrypted with the provider you name in state.encryptionProvider. Utila documents awskms://..., gcpkms://... and passphrase. A passphrase comes from the Secret in state.passphraseSecret.
If you lose the state, the agent cannot sign. Recovery is a new enrolment: register a new device, have the vault admins approve it in the Utila mobile app, and download a new key share. Plan a backup policy for the state store that matches how quickly you must recover. Your recovery time is the time to restore the state store, or to enrol a new device and have the vault admins approve it.
The service account key has two sources. Prefer service, which sets UTILA_SA_KMS_KEY_URL. The fallback privateKeySecret sets UTILA_SA_PRIVATE_KEY from a Secret. With the fallback, the key sits in the pod environment, so anyone who can read pod specs with secret access or exec into the pod can read it.
Annotate the agent's ServiceAccount through service: eks. for IAM roles for service accounts on EKS, iam. for GKE Workload Identity.
The Utila CLI talks to api.utila.io by default. For another Utila environment, set UTILA_HOST in extraEnv.
These UTILA_* names are Utila's own. They are separate from the credentials you enter for the Utila provider in the Console.
Availability
The agent runs one replica. While it is down, no Utila transaction that DALP filed gets signed, and DALP writes through Utila wait.
The PodDisruptionBudget defaults to max. A node drain that would evict the agent is blocked until you act, so writes pause only when you choose to. Set pod to 1 to let a drain proceed. The agent then stops, and designated work waits in AWAITING_SIGNATURE until the pod is back.
Kubernetes has no health signal for the agent. The Utila documentation lists no liveness endpoint or command for the co-signer, so the chart defines no probe. A hung agent looks healthy to Kubernetes. The health check is the work itself: alert when work DALP designated to the service account stays in AWAITING_SIGNATURE longer than the bound you accept. The runbook says what to do when the alert fires.
This page gives no detection time for an unreachable DALP endpoint, because DALP has not measured it against a live Utila vault. The Utila documentation says an unreachable endpoint leaves transactions pending and the co-signer asks again on the next poll.
Network policy
The chart renders a NetworkPolicy for the agent pod, on by default. It allows no ingress. Egress goes to:
- DNS.
- The DALP API pods on the port set in
dapi.service.httpPort. - Any rule you add in
networkPolicy.egress.
Add egress rules for the Utila API and for your KMS or object store. Without them the agent cannot poll Utila or open its state. The agent's pod carries the label app.. The DALP API NetworkPolicy admits pods of the same release with that label. The entry matches nothing unless the chart renders the agent pod.
The decision route lives under /internal, not under /api. No Ingress, HTTPRoute or OpenShift Route published by the chart carries an /internal path, and the Console does not proxy it. With the default /api and /dalp edge paths, a request from outside the cluster cannot reach the route. Do not publish / or an unset path at the edge, because either covers /internal too.
OpenShift
values-openshift.yaml pins the agent resources and leaves the user and group ids unset, so the restricted security context constraint assigns them. The pod takes its security context from the same shared settings as the other chart workloads and runs with a read-only root filesystem. The overlay clears fsGroup, so a PVC root can be unwritable under the assigned user id. On OpenShift, point state.location at object storage.
Verify the render
- Run
helm templatewith the agent enabled and the values above. Confirm a Deployment, a PodDisruptionBudget and a NetworkPolicy, and no Service, Ingress, HTTPRoute or Route that names the agent. - Confirm the decision address in the rendered arguments ends in
$(SIGNING_AGENT_SECRET)and the secret value appears in no rendered object except a chart-created Secret. - Render once without
image.repository. The render must fail with the message that names it.