?

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.com for 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-blob inside 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 Secret or 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

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.

Type to search 150+ pages. Ranking: BM25 with title/description boost.