Scenario
You run rousseau alongside other in-cluster services. You want the same runtime posture as the Podman + Quadlet reference — non-root, read-only root filesystem, all capabilities dropped, seccomp default — expressed as Kubernetes manifests. No inbound HTTP surface, so no Service and no Ingress.
Namespace + PodSecurity profile
Rousseau's posture satisfies the Pod Security Standards restricted profile. Set that as the namespace default:
apiVersion: v1
kind: Namespace
metadata:
name: rousseau
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
Secret
Provider credentials belong in a Secret, not the ConfigMap.
apiVersion: v1
kind: Secret
metadata:
name: rousseau-secrets
namespace: rousseau
type: Opaque
stringData:
ANTHROPIC_API_KEY: "sk-ant-..."
# Add SLACK_APP_TOKEN, SLACK_BOT_TOKEN, etc. as needed.
For Bedrock / Vertex, prefer workload identity (IRSA on EKS, Workload Identity on GKE) over long-lived keys.
ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: rousseau-config
namespace: rousseau
data:
config.yaml: |
provider: anthropic
anthropic:
model: claude-sonnet-4-6
max_tokens: 4096
log:
level: info
format: json
state:
path: /var/lib/rousseau/sessions.db
agent:
max_iterations: 32
approver:
mode: pattern
default: deny
allow:
- {tool: read, match: ".*"}
- {tool: grep, match: ".*"}
- {tool: edit, match: "^/workspace/.*"}
deny:
- {tool: bash, match: "rm -rf|sudo|curl|wget"}
slack:
# tokens come from the Secret via env vars
allowlist:
- U0123456789
PersistentVolumeClaim
Session state lives on disk. The claim ensures durability across pod rescheduling.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: rousseau-state
namespace: rousseau
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 5Gi
# Set your storageClassName as appropriate.
Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: rousseau-agent
namespace: rousseau
spec:
replicas: 1 # single-writer; SQLite session store
strategy: { type: Recreate } # avoid two pods sharing the PVC
selector:
matchLabels: { app: rousseau-agent }
template:
metadata:
labels: { app: rousseau-agent }
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile: { type: RuntimeDefault }
containers:
- name: rousseau
image: ghcr.io/sebastienrousseau/rousseau-agent:<pin-a-tag>
imagePullPolicy: IfNotPresent
args: ["slack"] # or "whatsapp", "discord", etc.
env:
- name: ROUSSEAU_ANTHROPIC_API_KEY
valueFrom:
secretKeyRef: { name: rousseau-secrets, key: ANTHROPIC_API_KEY }
- name: HOME
value: /home/rousseau
volumeMounts:
- { name: config, mountPath: /etc/rousseau, readOnly: true }
- { name: state, mountPath: /var/lib/rousseau }
- { name: tmp, mountPath: /tmp }
- { name: home, mountPath: /home/rousseau }
command: ["/usr/local/bin/rousseau"]
args:
- --config
- /etc/rousseau/config.yaml
- slack
resources:
requests: { cpu: "100m", memory: "128Mi" }
limits: { cpu: "1000m", memory: "512Mi" }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
volumes:
- name: config
configMap: { name: rousseau-config }
- name: state
persistentVolumeClaim: { claimName: rousseau-state }
- name: tmp
emptyDir: { medium: Memory, sizeLimit: 64Mi }
- name: home
emptyDir: { medium: Memory, sizeLimit: 16Mi }
The important bits:
| Field | Rationale |
|---|---|
runAsNonRoot: true + runAsUser: 1000 |
Matches the container image; the daemon never needs UID 0. |
readOnlyRootFilesystem: true |
The binary cannot mutate its own image. |
capabilities.drop: [ALL] |
No transport needs elevated capabilities. |
allowPrivilegeEscalation: false |
Blocks setuid escalation paths. |
seccompProfile: RuntimeDefault |
Kernel-level syscall gating. |
automountServiceAccountToken: false |
Rousseau does not talk to the Kubernetes API. |
replicas: 1 + Recreate |
SQLite is single-writer; two pods on the same PVC will corrupt the store. |
emptyDir for /tmp and $HOME |
Writable scratch that never leaves the pod. |
No Service, no Ingress
Every shipped transport uses either an outbound WebSocket (Slack Socket Mode, Discord Gateway), a subprocess (Signal), or polling (Telegram, Matrix, iMessage, Email). WhatsApp uses whatsmeow's outbound TCP session. There is no inbound HTTP surface, so no Service and no Ingress are required.
If you enable the MCP server (rousseau mcp), that is stdio-only — attach an MCP client via kubectl exec or side-car it into the same pod.
Egress policy
If you run a NetworkPolicy denying egress by default, allow rousseau to reach:
- The LLM provider endpoint (
api.anthropic.comfor Anthropic, region-specific Bedrock / Vertex URLs, or your internal vLLM if that's the target). - Each enabled transport's endpoints (Slack:
wss://wss-primary.slack.com, Discord:wss://gateway.discord.gg, WhatsApp: whatsmeow's TCP endpoints, etc.). - The transparency log endpoints only if you run
cosign verify-blobinside the pod — normally you verify at image-build time, not runtime.
Helm (roadmap)
A first-party Helm chart is on the roadmap. Draft values.yaml:
image:
repository: ghcr.io/sebastienrousseau/rousseau-agent
tag: v0.6.0
pullPolicy: IfNotPresent
transport:
name: whatsapp
args: ["--allow", "447900123456@s.whatsapp.net"]
provider:
name: anthropic
# API key sourced from an existing secret; do not template raw values.
existingSecret: rousseau-anthropic
existingSecretKey: api_key
persistence:
enabled: true
size: 4Gi
storageClassName: fast-ssd
resources:
requests: { cpu: "100m", memory: "128Mi" }
limits: { cpu: "1", memory: "512Mi" }
networkPolicy:
enabled: true
egressCIDRs:
- 3.5.0.0/16 # Anthropic (illustrative — CIDRs shift)
- 157.240.0.0/16 # Meta
serviceAccount:
create: true
annotations: {} # e.g. eks.amazonaws.com/role-arn for IRSA
podSecurityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
containerSecurityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: [ALL] }
seccompProfile: { type: RuntimeDefault }
Track docs/GAP_ANALYSIS_2026.md for chart availability.
ArgoCD application manifest
For GitOps deployments:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: rousseau-agent
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/your-org/platform-manifests
path: rousseau-agent
targetRevision: main
destination:
server: https://kubernetes.default.svc
namespace: agents
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
NetworkPolicy example
Restrict egress to only the LLM provider and the transport backends:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: rousseau-egress
namespace: agents
spec:
podSelector:
matchLabels: { app: rousseau-agent }
policyTypes: [Egress]
egress:
# DNS to CoreDNS
- to:
- namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: kube-system } }
podSelector: { matchLabels: { k8s-app: kube-dns } }
ports:
- protocol: UDP
port: 53
# Anthropic API
- to:
- ipBlock: { cidr: 3.5.0.0/16 } # illustrative CIDR
ports:
- protocol: TCP
port: 443
Caveats
- SQLite requires a single writer. Do not scale replicas above 1. If you need HA, run a passive standby with a fast failover script — do not run two active writers against the same PVC.
- Session store is not encrypted at rest. Mount the PVC on an encrypted storage class if required by policy.
- Provider auth material should always live in
Secretor workload identity, never in the ConfigMap.
Troubleshooting
PodSecurity restricted violation on apply
Your namespace enforces the restricted PodSecurity profile and the manifest is missing runAsNonRoot, a seccomp profile, or has allowPrivilegeEscalation: true. The manifests above set all of these — cross-check line by line.
PVC stuck in Pending
Your storage class does not provision automatically, or the requested size exceeds the quota. Check with kubectl describe pvc rousseau-state.
Pod cannot resolve api.anthropic.com
DNS not allowed through your NetworkPolicy. Add an egress rule to kube-dns on UDP/53.
IRSA / Workload Identity: NoCredentialProviders
Service account annotation is wrong or missing. Verify with kubectl get sa rousseau -n agents -o yaml — the annotation must be an ARN (AWS) or a iam.gke.io/gcp-service-account (GCP).
Rolling out a new image kills the old pod but the new one never becomes Ready
The Recreate strategy means the old pod stops before the new one starts. If the new image cannot start (bad config, missing secret), you have an outage. Fix the config first, then re-apply.
Related pages
- Deployment — the reference Podman + Quadlet posture.
- Guides: Observability — wire slog output into your log pipeline.
- Guides: Audit & Approval Policies — the safety posture you present to auditors.
- Providers: Bedrock — IRSA setup for AWS.
- Providers: Vertex — Workload Identity setup for GCP.
Further reading
docker/Dockerfile— the image the manifests reference.docker/rousseau-agent.container— the Quadlet equivalent (for a non-K8s alternative).- Kubernetes docs: PodSecurityStandards.
- Kubernetes docs: NetworkPolicy.