Skip to main content

Run the Utila signing agent from the Helm chart

Deploy the customer-run Utila signing agent beside DALP: chart values, image, state, keys, and the network path that keeps the decision route internal.

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 reaches DALP from inside the cluster, and the public edge never routes the decision route.Inside one cluster, the signing agent pod has an accent arrow to the DALP API Service. The arrow is labelled decision request. The agent also has a dashed arrow leaving the cluster to the Utila API, which holds the transactions it polls. Outside the cluster, a public edge box shows the paths it publishes, the API path and the DALP path, with an arrow to the DALP API Service. A crossed note says the internal path is not published, so a request from outside the cluster for the decision route is never routed.The agent reaches DALP from inside the cluster, and the public edge never routes the decision route.The same network path drawn portrait. Inside the cluster, the signing agent pod asks the DALP API Service for a decision with an accent arrow. The agent also reaches the Utila API outside the cluster with a dashed arrow. The public edge publishes only the API paths and does not route the decision route.

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:

Object
What it does
Deployment
One replica with the Recreate strategy. Two agents on one key share are unsafe, so the old pod stops before the new one starts.
ServiceAccount
No API token is mounted into the pod.
PodDisruptionBudget
maxUnavailable from utilaSigningAgent.podDisruptionBudget.maxUnavailable. The default is 0.
NetworkPolicy
No ingress. Egress to DNS, the DALP API pods and the rules you add.
PersistentVolumeClaim
The agent state, when state.location and state.persistence.existingClaim are both empty. Kept on upgrade and uninstall.
Secret
Only when signingAgentSecret.create is true. Use it outside production only.

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 utilaSigningAgent.enabled is true:

Value
Meaning
image.repository, image.digest
The image you built.
organizationId
The DALP organisation id of the Utila provider configuration.
providerConfigurationId
The id of that provider configuration.
vaultId, deviceId
The Utila vault and the device id printed at registration.
account
The Utila service account email, passed as UTILA_ACCOUNT.
serviceAccountKey
kmsKeyUrl, or privateKeySecret.name.
signingAgentSecret.existingSecret
The Secret that holds the agent secret. Required in webhook-decision mode, unless signingAgentSecret.create is true.
state.encryptionProvider
How Utila encrypts the state.
networkPolicy.egress
Egress rules for the Utila API and your KMS or object store. Required while networkPolicy.enabled 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 --vault-id <vault-id> --device-id <device-id> --webhook-url <url>. 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-all 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.

Choice
Value
Notes
Chart volume
state.location empty
A PVC mounted at /state. Back it up.
Object storage
s3://bucket or gs://bucket
No PVC is rendered. Use it on OpenShift, as OpenShift explains.

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 serviceAccountKey.kmsKeyUrl, 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 serviceAccount.annotations: eks.amazonaws.com/role-arn for IAM roles for service accounts on EKS, iam.gke.io/gcp-service-account 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 maxUnavailable: 0. A node drain that would evict the agent is blocked until you act, so writes pause only when you choose to. Set podDisruptionBudget.maxUnavailable 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.kubernetes.io/component: utila-signing-agent. 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

  1. Run helm template with 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.
  2. 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.
  3. Render once without image.repository. The render must fail with the message that names it.

Was this page helpful?