Security¶
Purko's security model is layered: Kubernetes RBAC controls who can create and modify CRDs, pod security contexts isolate execution, autonomy levels control what agents do at runtime, and cost/iteration limits act as circuit breakers against runaway execution.
Pod Isolation¶
Every workflow step runs as an isolated Kubernetes Job with its own pod. This means:
- Blast radius is bounded per step. A step that fails, hangs, or misbehaves affects only its own pod. It does not share a process namespace or filesystem with other steps.
- No shared state between steps. Output passes through the controller via pod logs → ConfigMap → environment variables. Steps cannot directly read each other's memory or filesystem.
- Automatic cleanup. Jobs are garbage collected 3600 seconds after completion (
ttlSecondsAfterFinished). Pod logs are read by the controller before cleanup. - No retry amplification.
backoffLimit: 0means Kubernetes will not retry a failed job pod. Retry decisions are made by the Workflow controller, which can apply back-off and failure strategies.
RBAC Model¶
Operator ClusterRole¶
The operator runs as ServiceAccount purko-operator in the purko-system namespace. It is bound to a ClusterRole with the following permissions (from deploy/helm/templates/rbac.yaml):
| API Group | Resources | Verbs | Purpose |
|---|---|---|---|
purko.io |
agents, agents/status, agents/finalizers |
all | Manage Agent CRs |
purko.io |
workflows, workflows/status, workflows/finalizers |
all | Manage Workflow CRs |
batch |
jobs |
all | Create and delete step Jobs |
"" (core) |
pods, pods/log |
get, list, watch | Read executor pod logs for output capture |
"" (core) |
configmaps |
all | Output ConfigMaps, MCP registry, presets, trigger rules |
"" (core) |
secrets |
get, list, watch | Read credentials for LLM providers and MCP auth tokens |
apps |
deployments |
get, list, watch | Dashboard overview of MCP server deployments |
autoscaling |
horizontalpodautoscalers |
get, list, watch | Dashboard status display |
coordination.k8s.io |
leases |
all | Leader election |
"" (core) |
events |
create, patch | Emit Kubernetes events for audit trail |
The operator does not hold create on secrets — it reads existing secrets but never writes them. Credentials are managed by the user and referenced by name.
Per-Agent RBAC¶
The Agent controller provisions a Role and RoleBinding in the agent namespace for each agent. This restricts what the agent's executor pod can do within the cluster. The scope is limited to the agent's own namespace and only to resources the agent needs.
User-Facing Roles¶
Two example roles for human operators (from the Security guide):
# Full access to manage agents and workflows
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: purko-operator
namespace: ai-agents
rules:
- apiGroups: ["purko.io"]
resources: ["agents", "workflows"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Read-only access for observers
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: purko-viewer
namespace: ai-agents
rules:
- apiGroups: ["purko.io"]
resources: ["agents", "workflows"]
verbs: ["get", "list", "watch"]
- apiGroups: ["purko.io"]
resources: ["agents/status", "workflows/status"]
verbs: ["get"]
Operator Pod Security¶
The operator Deployment enforces strict security contexts (from deploy/helm/values.yaml):
Pod-Level¶
UID 65532 is the nonroot user in distroless/static images, the standard base image for Go operators built with kubebuilder. Running as a known non-root UID satisfies most PodSecurityAdmission policies without requiring privilege escalation.
Container-Level¶
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
allowPrivilegeEscalation: false— the process cannot gain more privileges than its parent, even if the binary has setuid bits.readOnlyRootFilesystem: true— the container filesystem is mounted read-only. Temporary writes go to explicitly mounted volumes oremptyDirmounts.capabilities: drop: ALL— all Linux capabilities are removed. The operator binary does not require any capability (no raw sockets, no bind to privileged ports, no filesystem ownership changes).
Additional Hardening¶
The Helm chart also supports:
RuntimeDefaultseccomp profile (system call filter)- Pod Disruption Budget (
minAvailable: 1) for HA deployments - Liveness probe at
/healthz:8081with 15s initial delay - Readiness probe at
/readyz:8081with 5s initial delay
Autonomy as a Security Boundary¶
The Shu-Ha-Ri autonomy system is not just a UX feature — it is a runtime enforcement mechanism that limits what agents can do before humans have established trust.
Three Levels¶
| Level | Tool Access | Human Gate |
|---|---|---|
shu |
Read-only tools only | Every step requires purko.io/approve-{step}=true annotation |
ha |
All tools in agent spec | Runs automatically; human notified of results |
ri |
All tools in agent spec | Fully autonomous; no notification required |
At shu level, the executor filters out any tool that modifies state before presenting the tool list to the LLM. The LLM cannot call write tools even if it generates a call — the executor rejects it before making the MCP request. This is enforced inside the pod, independently of RBAC.
Annotation-Based Approval (Shu Level)¶
The Workflow controller checks for a step approval annotation before creating the Job:
If the annotation is absent, the step status is set to Waiting for approval and the controller requeues. No Job is created. When an operator adds the annotation (via kubectl annotate or the dashboard), the next reconciliation cycle picks it up and creates the Job.
This creates a human-in-the-loop gate that does not require any out-of-band approval system — it uses the Kubernetes object itself as the approval record.
Promotion and Rollback¶
The Autonomy controller automatically adjusts autonomy levels based on observed behavior:
- Promotion (
shu → ha,ha → ri): triggered when success rate and action count thresholds inAgentAutonomyPolicy.spec.promotionCriteriaare met. - Demotion (
ha/ri → shu): triggered whenrollbackTriggersfire (e.g., success rate drops below 80%, three consecutive failures).
Automatic promotion requires requiredApprovals: 0. Setting requiredApprovals: 1 means a human must explicitly approve the promotion, even if performance criteria are met.
See Shu-Ha-Ri concept for configuration examples.
Cost and Iteration Limits¶
Cost limits and iteration caps are enforced inside the executor pod, not just declared in the spec. They act as circuit breakers:
| Guardrail | Env Var | Effect |
|---|---|---|
maxIterations |
MAX_TOOL_CALLS |
Executor stops the ReAct loop after N tool calls, regardless of task completion |
costLimitUSD |
COST_LIMIT_USD |
Executor accumulates per-call cost from LLM API responses; stops when limit exceeded |
maxExecutionTime |
MAX_EXECUTION_TIME |
Passed as the Job activeDeadlineSeconds; Kubernetes kills the pod hard |
A runaway LLM that generates an infinite tool-call loop will be stopped by maxIterations before it exhausts the cost limit. The activeDeadlineSeconds on the Job acts as a final hard stop if the executor process itself hangs.
MCP Server Authentication¶
MCP servers authenticate callers via bearer tokens injected from Kubernetes Secrets. The MCPServer controller reads the token from a referenced Secret and passes it to the executor via the MCP_SERVERS JSON array. The executor includes the token in the Authorization: Bearer {token} header on each JSON-RPC request.
Token rotation requires updating the Secret. The MCP registry re-reads the ConfigMap (and therefore the Secret reference) on each 60-second sync cycle, so updated tokens take effect within one cycle without restarting the operator.
Credential Management¶
Credentials for LLM providers are stored in Kubernetes Secrets and referenced by name in the Agent or LLMProvider spec:
The job builder injects the secret value as MODEL_API_KEY using a secretKeyRef env var — the value is never written to a ConfigMap or pod spec field as a plain string. The operator itself only reads secrets; it does not create or modify them.
For Vertex AI, a GCP service account JSON key is stored in a Secret and mounted as a file at /var/run/secrets/gcp/credentials.json. The executor reads this path via the standard GOOGLE_APPLICATION_CREDENTIALS environment variable.
Admission Webhook TLS¶
With webhooks.enabled: true, the chart deploys validating webhooks for
Agents and Workflows. Webhook TLS is provisioned one of two ways:
- Chart-generated certificates (default): Helm generates a CA and serving
certificate at install time (
genCA/genSignedCert), stores them in thepurko-webhook-server-certSecret, and injects the CA into theValidatingWebhookConfigurationcaBundle. Existing certificates are reused on upgrade (vialookup), so they are not rotated on everyhelm upgrade. - cert-manager (
webhooks.certManager.enabled: true): the chart creates a self-signed Issuer and Certificate instead, and cert-manager's CA injector maintains thecaBundle. Use this in production clusters that already run cert-manager.
The serving certificate is mounted read-only at
/tmp/k8s-webhook-server/serving-certs, where controller-runtime's webhook
server expects it. All webhooks use failurePolicy: Fail — invalid resources
are rejected even if the operator is briefly unavailable.
Network Considerations¶
MCP servers run as ClusterIP services in production. The executor pods connect to them via DNS (http://{service}.{namespace}.svc.cluster.local:{port}). No external ingress is required for MCP communication.
For local development (minikube), the operator and MCPServer controller support hostNetwork: true, which places pods on the host network. This simplifies local port access but should not be used in production.
The operator's dashboard (:8082) is exposed as a ClusterIP Service by default. In production, expose it via an Ingress or kubectl port-forward — do not expose it to the internet without authentication.
Security Best Practices¶
- Use
autonomyLevel: restrictedfor new agents. Only promote after observing successful runs in staging. - Set
blastRadiusLimit.maxAffectedResourcesanddeniedActions. Prevents an agent from modifying more resources than intended. - Always use
credentialsSecretRef, never embed keys in Agent specs. Secrets referenced by name are not included inkubectl get agent -o yamloutput from the API server. - Enable
guardrails.audit: truefor privileged agents. Audit log entries are written as Kubernetes events and can be forwarded to your SIEM. - Set network policies. Restrict executor pod egress to only the LLM endpoints and MCP servers each agent needs.
- Do not expose the dashboard without authentication in production environments.
Related Pages¶
- Overview — system diagram, namespace model
- Controllers — RBAC provisioning by Agent controller
- Shu-Ha-Ri — autonomy levels and progression
- AgentAutonomyPolicy CRD — promotion and rollback criteria
- Agent CRD — guardrails and blast radius fields