Repository navigation
Deploy Kubernetes
Experimental: this works and is tested, but may change and gets less hardening than the stable core.
deploy/helm/tracekit-signer renders one agent Deployment with the signer (the image of
deploy-container.md) as a native sidecar: an init container with restartPolicy: Always, so it
is up before the agent starts and outlives it. Kubernetes 1.29 or later.
helm install my-agent deploy/helm/tracekit-signer \
--set agent.image=registry.example/agent@sha256:... \
--set signer.image=ghcr.io/cygnux-labs/tracekit-signer@sha256:...
kubectl exec deploy/my-agent -c tracekit-signer -- tracekit doctor --config /etc/tracekit/signer.yamlWhat the chart sets up:
| Signer (sidecar) | Agent | |
|---|---|---|
| user | uid/gid 10001 (signer.uid) |
uid 1000 (agent.uid); the chart refuses 0 and the signer's uid |
socket dir /run/tracekit-signer (emptyDir, 1 MiB) |
mounted | mounted read-only, TRACEKIT_SIGNER=/run/tracekit-signer/signer.sock
|
keys and log (PVC, persistence) |
mounted | not mounted |
signer.yaml and policy.yaml (ConfigMap, subPath, root-owned) |
mounted | not mounted |
record key Secret (recordKey.secretName) |
mounted | not mounted |
The socket directory is the only volume the two share, read-only in the agent's container: connecting needs write
access to the 0666 socket only, so the agent can't unlink or replace the socket or put anything at its path. The signer knows the agent by its peer uid: signer.yaml's
tenants maps uid:<agent.uid> to signer.tenant. Both containers run with runAsNonRoot, readOnlyRootFilesystem,
allowPrivilegeEscalation: false, all capabilities dropped and the RuntimeDefault seccomp profile. If your agent
needs a writable path, mount an emptyDir of its own.
Values (values.yaml documents each):
-
signer.config: more signer.yaml keys, e.g.witnesses. The chart's own keys (data_dir,socket,socket_mode,durability,tenants,metrics,policy) win over them. -
signer.policy: the signer's policy file, mounted at/etc/tracekit/policy.yaml. The default,extends: packs/server.yaml, runs the server pack the image ships in/etc/tracekit/packs(policy-v2.md); replace it with your own, extendingpacks/server.yamlto keep its rules. The same value in the central chart sets every replica's policy. -
signer.resources,agent.resources(central:signer,viewer,monitor,issuer.resources): CPU and memory requests and limits; the defaults suit a light load, so size them for yours. -
persistence: the chart makes aReadWriteOncePVC kept on uninstall, or usesexistingClaim. The volume's root must be writable by uid 10001; the signer keeps its data in a 0700data/directory it makes there. The chart sets nofsGroup: that would add the signer's group to the agent's container too.persistence.enabled: falsekeeps the log in an emptyDir that dies with the pod: for development only (the install notes warn). -
recordKey.secretName: a Secret whoserecord.key(32 bytes) becomes the signer's record key on first start. A later start with a different Secret is refused. Without it the signer makes its own key in the data volume. -
metrics.remoteserves metrics on the pod IP (port 9464) instead of loopback;networkPolicy.enabledthen admits ingress to the pod only on 9464 fromnetworkPolicy.monitoringNamespace. That policy covers the whole pod, the agent's ports included. -
automountServiceAccountTokenisfalse: the pod gets no API token unless you turn it on.
No cloud credentials in the pod. GKE Workload Identity, EKS Pod Identity and the like give a pod's credentials to every container in it, the agent's too, so a KMS log key in the sidecar would be one the agent can use. A KMS log key belongs to the central signer, where the agent's pod holds no credentials for it.
The deployment runs one replica with the Recreate strategy: the signer holds its data volume's storage lock.
deploy/helm/e2e.sh installs the chart on a throwaway kind cluster, registers a run and decides a call from the agent
container, checks the agent cannot list the signer's keys, and runs doctor in the sidecar. Then it stops the signer
mid-run and checks that a fail-open call made while it was down shows as a signed client_counter_gap once it is back.
It needs docker, kind, kubectl and helm. tests/test_helm.py checks the rendered manifests where helm is installed.
deploy/helm/tracekit-central runs the signer as a service that agents in other pods reach over HTTPS. It is a
StatefulSet: replica n is the only writer of log n. Its config (signer-<n>.yaml, picked by the pod's ordinal), its
Postgres schema, its log key and its volume are n's alone, and Postgres holds the log's advisory lock for it, so two pods
never write one log.
helm install tk deploy/helm/tracekit-central --namespace tracekit \
--set signer.image=ghcr.io/cygnux-labs/tracekit-signer@sha256:... \
--set tls.secretName=tk-tls \
--set awsKms.region=eu-west-1 --set 'awsKms.keyIds={alias/tk-log-0,alias/tk-log-1}' \
--set serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn=arn:aws:iam::...:role/tk-signerBefore installing, for each replica n (0 up to replicas, or autoscaling.maxReplicas with the HPA):
- a Postgres schema, migrated with
tracekit signer migrateas a migration role, and a signer role holdingtracekit.storage.postgres.GRANTSon it; - Secret
tracekit-log-<n>(postgres.dsnSecretPrefix) whosedsnkey is that role's DSN, its search_path the schema (options=-csearch_path=tk_log_n); - with
awsKms, an Ed25519 KMS key (awsKms.keyIds[n]) the signers' role may sign with. Without it, the log key is a file in the replica's volume.
tls.secretName is a kubernetes.io/tls Secret whose certificate names tk.tracekit.svc and tk-<n>.tracekit.svc for
every replica.
| identity | HTTPS (:8443) with k8s_sa: agents send a projected service-account token of audience tracekit-signer (audience), checked by TokenReview (the signers are bound to system:auth-delegator). Map identities to tenants and grants in signer.config (tenants, authorize). |
| routing | a new run goes to any ready replica (Service tk); its run id, and its approvals' ids, start with the replica's name, tk-<n>., and the Python and TypeScript clients send every later call for them to Service tk-<n>. A run stays on the replica that registered it, also when another process resumes it. A client's own run_id must carry the prefix of the replica it names. Calls that name no run or approval (status, approval_list without a run) reach one replica: list each replica's approvals through its own Service (tk-<n>). replicas is at least 1: a replica scaled away closes its log for good. |
| containment | holds, quarantines and a paused response engine are each replica's own (incidents); suspensions are shared through suspensions:, a file on a volume every replica mounts. Send tracekit signer incidents list, pause and unpause to every replica (--signer per tk-<n>). |
| cloud credentials | only on the signers' ServiceAccount (serviceAccount.annotations, for workload identity to the KMS keys). Agent pods, the viewer and the monitor get none. |
| metrics |
:9464 on each replica: Prometheus, and with the monitor the record entries it reads. The chart's NetworkPolicy admits it only from networkPolicy.monitoringNamespace and the monitor. |
Agents:
env:
- {name: TRACEKIT_SIGNER, value: "https://tk.tracekit.svc:8443"}
- {name: TRACEKIT_SIGNER_TOKEN_FILE, value: /var/run/secrets/tracekit/token}
- {name: TRACEKIT_SIGNER_CA, value: /etc/tracekit-ca/ca.crt}
volumes:
- name: tracekit-token
projected: {sources: [{serviceAccountToken: {audience: tracekit-signer, expirationSeconds: 3600, path: token}}]}Scaling. Scaling up adds replica n and with it log n. Scaling down closes the last replica's log for good: its preStop
hook sees its ordinal past the StatefulSet's replicas, and on SIGTERM the signer (serve --close-on-stop) writes
log.closed and the final notes, and waits up to 60 s for every witness to cosign them, so the log ends in a cosigned
note with no unproven tail. signer.terminationGracePeriodSeconds (120) covers it. A restart, an upgrade or an
uninstall closes nothing. A closed log takes no more records, and a signer refuses to serve one: before ordinal n runs
again, give it a new log (a new schema and DSN Secret, a new KMS key and origin). That is why the HPA only scales up
unless autoscaling.scaleDown is set.
The HPA scales on tracekit_signer_queue_depth averaged over the replicas, through
prometheus-adapter, for example:
rules:
custom:
- seriesQuery: 'tracekit_signer_queue_depth{namespace!="",pod!=""}'
resources: {overrides: {namespace: {resource: namespace}, pod: {resource: pod}}}
metricsQuery: 'max_over_time(<<.Series>>{<<.LabelMatchers>>}[1m])'Optional parts:
-
issuer.enabled: the record-key issuer (issuer.md) withissuer.config(its issuer.yaml withoutdata_dirandhttp), on:8444withk8s_sa. Point the signers'record_keyathttps://tk-issuer.tracekit.svc:8444insigner.config. Its own ServiceAccount takes the workload identity of a KMSca_key. -
viewer.enabled: onetracekit viewof every log on port 7778 (viewer.md), read with the DSNs of Secretstracekit-log-read-<n>: a role holdingREAD_GRANTSonly (SELECT).viewer.configadds to its viewer.yaml, such asview.oidcand thehttpsection whoseoidcissuers it names, for OIDC logins. -
monitor.enabled:tracekit monitorof each log,monitor.logKeys[n]being replica n's vkey (kubectl exec tk-<n> -- tracekit signer vkey --config /etc/tracekit/central/signer-<n>.yaml).
The central chart also runs the services that hold credentials for the agents, each in its own pod with its own ServiceAccount, its credentials from a Secret mounted only there:
| values | service | port | Secret (secretName, at /etc/tracekit/secrets/) |
|---|---|---|---|
gateway |
LLM gateway | 8080 | the provider key, as api_key
|
executor |
tool executor | 8090 | the files the actions' headers name |
mcpGateway |
MCP gateway | 8091 | the files the upstreams' env and headers name |
Each takes its config in config (without http and signer, which the chart sets), serves the agents on HTTPS with
k8s_sa (tlsSecretName: a certificate for tk-<name>.tracekit.svc; the pods are bound to system:auth-delegator
for TokenReview), and reaches the signers with a projected token of audience and the ca.crt of tls.secretName
(only that key is mounted). In signer.config, list their identities under gateways and grant them their calls:
signer:
config:
gateways: ["k8s_sa:system:serviceaccount:tracekit:tk-gateway", "k8s_sa:system:serviceaccount:tracekit:tk-executor",
"k8s_sa:system:serviceaccount:tracekit:tk-mcp-gateway"]
authorize:
"k8s_sa:system:serviceaccount:tracekit:tk-gateway": [model_event, close_run]
"k8s_sa:system:serviceaccount:tracekit:tk-executor": [decide, complete, approval_request, approval_wait, approval_consume]
"k8s_sa:system:serviceaccount:tracekit:tk-mcp-gateway": [decide, complete, approval_request, approval_wait, approval_consume, observe]Each takes the agents' service-account tokens of its own audience, <audience>-<name> (tracekit-signer-executor
for the executor by default), never the signers' audience, so a token a custody service receives can't be replayed at
the signers. An agent pod mounts a projected token per service it calls, next to the one it sends the signers:
volumes:
- name: executor-token
projected: {sources: [{serviceAccountToken: {audience: tracekit-signer-executor, expirationSeconds: 3600, path: token}}]}Custody holds only if the agents can't reach the APIs around these services. agentEgress.enabled adds a
NetworkPolicy for the agent pods (agentEgress.podSelector, in agentEgress.namespace) that lets them reach only the
signers, the enabled custody services, the egress proxy (agentEgress.egressProxy) and DNS. egress.enabled runs
the chart's own egress observer as <release>-egress on 3128, which the policy then allows too: the
agents' outside traffic goes through it and each run records the hosts it reached. Clients name their run with its
proxy_env (TRACEKIT_EGRESS_PROXY=https://<release>-egress.<namespace>.svc:3128, with
NO_PROXY=.svc,.cluster.local); a plain HTTPS_PROXY without a run token is refused (407), since the observer
knows a networked client only by what it sends. Grant its identity observe_effect and set
posture: {network: enforced} in signer.config:
agentEgress:
enabled: true
namespace: agents
podSelector: {app: support-agent}It is off by default: turning it on cuts every other connection the agents make. A NetworkPolicy's egress rules hold
only on a CNI that enforces them (Calico and Cilium do; some managed defaults don't): check from an agent pod that an
outside address is refused, for example kubectl exec -n agents deploy/support-agent -- python -c "import socket; socket.create_connection(('api.github.com', 443), 3)" must time out. Keep the provider keys and action
secrets out of the agents' namespace altogether. The policy lets the agents reach kube-dns, which a DNS tunnel can carry
data through: filter DNS (a DNS-aware policy, a CoreDNS allow-list) where that matters. With NodeLocal DNSCache the
agents resolve through 169.254.20.10, which the policy doesn't allow: add an egress rule for 169.254.20.10/32 on
port 53.
tests/test_helm.py checks the rendered manifests, tests/test_central.py the routing and the close on scale-down.
Generated from docs/ at v1.4.1. Edit the docs in the repository, not here.
Get started
- Quickstart: the v2 signer
- Quickstarts
- Quickstart: Claude Agent SDK
- Quickstart: a custom agent
- Quickstart: LangChain and LangGraph
- Quickstart: MCP client
- Quickstart: OpenAI Agents SDK
- Coding agents other than Claude Code
- tracekit doctor
- Walkthrough: Tracekit end to end
How it works
- How Tracekit works
- Overview
- The life of a tool call
- The evidence format
- Verification and assurance
- Witnesses and monitoring
- Policy
- Approvals
- Detection and containment
- Capture and completeness
- Custody
- Provenance and tracekit why
- Deployment and isolation
- Storage, durability and performance
- Integrations
- Security model
- Architecture
- tracekit why: architecture
Integrations
- Integrations
- Adapters
- OpenTelemetry
- Remote clients
- Kubernetes agent-sandbox
- agentgateway
- agentsh and Tracekit
- Browser Use
- Claude Agent SDK
- Claude Code's sandbox
- Codex's sandbox
- CrewAI
- Docker MCP Gateway
- E2B
- Falco
- Gemini CLI's sandbox
- Google ADK
- Haystack
- LangChain v1
- LiteLLM
- LlamaIndex
- MCP client
- microsandbox
- Microsoft Agent Framework
- OpenAI Agents SDK
- NVIDIA OpenShell
- Pydantic AI
- sandbox-runtime's proxy on its own
- Strands Agents
- Tetragon
- Vercel AI SDK
- Wassette
Policy and approvals
- Policy reference
- Approvals
- Findings
- Reading the model's reasoning
- Incidents and containment
- Webhooks
Deploy and operate
- Deploying the signer
- The signer as a container
- The signer on Kubernetes
- The compose stack
- Identity
- Tool executor
- MCP gateway
- LLM gateway
- Egress observation
- Witnesses
- Log monitor
- Record-key issuer
- Signer observability
- Viewer
- tracekit why: causal investigation and replay
- Migrating
- Platforms
Verify the evidence
- Reading a v2 verify report
- Evidence format v2
- Auditor guide
- Sample evidence bundle
- Signing
- What Tracekit guarantees
Security and privacy
- Threat model: the v2 signer
- Threat model: the v1 laptop signer
- HTTP security checklist
- Threat-model review packet
- Privacy
- Known limits and trade-offs
- What's stable
Compliance
- Compliance mapping
- EU AI Act
- ISO/IEC 42001
- NIST AI RMF and the Generative AI Profile
- OWASP Top 10 lists for LLM and agentic applications
Evaluation
Demos
- Demos
- Agent approvers within bounds
- Completeness and tamper evidence
- Detection and containment
- Network coverage: an egress observer
- Guardrails: allow, deny, or ask a person
- MCP tools, gated and recorded
- Prompt injection held by provenance
- Why: which input caused the action
Help