Security
Last reviewed: March 21, 2026 — security documentation is current; removed Cilium due to OrbStack CNI constraint; updated PSS table accordingly.
This document is the consolidated security report for the homelab. It covers the overall security posture, per-layer controls, and a dedicated section on LLM/AI agent (OpenClaw) permissions. This report is tracked by the doc-freshness system and should be reviewed and updated with every release.
Overview
The homelab follows a defense-in-depth model across three dimensions: network isolation (Tailscale-only access, default-deny NetworkPolicies), workload hardening (Pod Security Standards, non-root containers, least-privilege RBAC), and secret hygiene (Infisical-managed secrets that never touch git). All cluster changes flow through GitOps (ArgoCD with self-heal), and all code changes require pull request review on a protected main branch.
Threat Model
The homelab operates under a constrained threat model that differs significantly from a production cloud environment:
| Factor | Detail |
|---|---|
| Operator | Single user (owner) — no multi-tenancy |
| Network boundary | Private Tailscale tailnet only — no public internet exposure |
| Devices on tailnet | 3 devices: Mac mini M4 (100.77.144.4), iPhone 12 Pro Max (100.67.153.52), iPad mini gen 5 (100.121.193.73) |
| Cluster topology | Single-node OrbStack Kubernetes on macOS (arm64) |
| Data sensitivity | Infrastructure credentials, API keys, personal git repos |
| Primary risks | Secret leakage, misconfigured RBAC, AI agent misbehavior, supply chain compromise |
flowchart TD
subgraph threats["Threat Vectors"]
T1["Secret leakage\n(git commit, env dump)"]
T2["AI agent misbehavior\n(unauthorized changes)"]
T3["Supply chain\n(compromised image, chart)"]
T4["Network intrusion\n(bypassing Tailscale)"]
end
subgraph mitigations["Mitigations"]
M1["Infisical + ESO pipeline\nSecrets never in git"]
M2["Branch protection + PR review\nAgent footprint tracing"]
M3["Trivy scanning\nImage provenance tracking"]
M4["Tailscale WireGuard\nDefault-deny NetworkPolicies"]
end
T1 --> M1
T2 --> M2
T3 --> M3
T4 --> M4
Since the tailnet is single-user with only owner-controlled devices, threats like unauthorized network access or multi-tenant privilege escalation are out of scope. The primary concerns are operational: accidental secret exposure, AI agent drift beyond intended scope, and supply chain integrity.
Network Security
No services are exposed to the public internet. All external access flows through Tailscale Serve, which provides WireGuard encryption and automatic Let's Encrypt TLS certificates.
Access Model
flowchart LR
Internet["Public Internet"] -. "BLOCKED" .-> Mac["Mac mini"]
subgraph tailnet["Tailscale Tailnet (private)"]
iPhone["iPhone"]
iPad["iPad"]
Mac
end
iPhone -- "WireGuard\nTLS via LE" --> Mac
iPad -- "WireGuard\nTLS via LE" --> Mac
| Control | Status |
|---|---|
| LoadBalancer services | None — not used |
| Ingress controller | None — not deployed |
| NodePort binding | localhost only (OrbStack constraint) |
| External access | Tailscale Serve (WireGuard + auto TLS) |
tailscale funnel (public) |
Disabled — all endpoints are tailnet-only |
Default-Deny Network Policies
Every application namespace has a default-deny-all NetworkPolicy that blocks all ingress and egress. Traffic is then explicitly allowed per namespace:
| Namespace | Foundational Policies | Namespace-Specific Rules |
|---|---|---|
argocd |
deny-all, allow-same-ns, allow-dns | Tailscale ingress (:8080), API server egress (:6443), internet egress (:443, :22) |
monitoring |
deny-all, allow-same-ns, allow-dns | Tailscale ingress (:3000), API server egress (:6443), internet egress (:443) |
authentik |
deny-all, allow-same-ns, allow-dns | Tailscale ingress (:9000, :9443) |
openclaw |
deny-all, allow-same-ns, allow-dns | Tailscale ingress (:18789), API server egress (:6443), internet egress (:443) |
external-secrets |
deny-all, allow-same-ns, allow-dns | API server egress (:6443), Infisical egress (:8080) |
infisical |
deny-all, allow-same-ns, allow-dns | Tailscale ingress (:8080), ingress from external-secrets (:8080) |
Tailscale ingress rules are locked to the CGNAT range 100.64.0.0/10, ensuring only tailnet devices can reach services.
OrbStack CNI limitation
OrbStack does not enforce NetworkPolicies at the CNI level. These policies serve as declarative intent and will be enforced if the cluster is migrated to a CNI that supports them (Cilium, Calico). They still provide value as documentation and as a GitOps-tracked security baseline.
Pod Security Standards
Namespaces are labeled with Kubernetes Pod Security Standards (PSS) to control what workloads can run:
| Namespace | Enforce | Audit/Warn | Reason |
|---|---|---|---|
argocd |
restricted |
restricted |
Fully compliant |
external-secrets |
restricted |
restricted |
Fully compliant |
monitoring |
baseline |
restricted |
node-exporter requires host namespaces and hostPort |
authentik |
baseline |
restricted |
server/worker containers run as root, missing seccompProfile |
infisical |
baseline |
restricted |
standalone + ingress-nginx run as root, missing seccompProfile |
launchfast |
restricted |
restricted |
nginx-unprivileged runs as non-root (UID 101), read-only rootfs |
openclaw |
— | — | Excluded: uses hostPath volumes disallowed by the restricted profile |
Namespaces at baseline enforce + restricted audit/warn log violations without blocking pods, surfacing non-compliant workloads in audit logs for future remediation.
RBAC
Every service runs under a dedicated ServiceAccount with least-privilege permissions. No service has cluster-admin access.
| Service | ServiceAccount | Scope | Permissions |
|---|---|---|---|
| OpenClaw | openclaw (ns: openclaw) |
Namespace Role + ClusterRole | See OpenClaw RBAC detail below |
| ArgoCD | argocd-* (ns: argocd) |
ClusterRole | Managed by Helm chart — application controller needs cluster-wide access to sync resources |
| ESO | external-secrets (ns: external-secrets) |
ClusterRole | Managed by Helm chart — needs cluster-wide access to create secrets in any namespace |
| Monitoring | kube-prometheus-stack-* |
ClusterRole | Managed by Helm chart — Prometheus needs cluster-wide metrics scraping |
OpenClaw's RBAC was expanded from a namespace-only Role (v1.1.0–v1.3.0) to a namespace Role + targeted ClusterRole in v1.4.0. The ClusterRole grants cluster-wide read and scoped operational writes (rollout restart, force-sync, ArgoCD hard refresh) without granting cluster-admin. See the full RBAC breakdown in the OpenClaw security section.
Secret Management
Secrets follow a strict pipeline: Infisical (source of truth) -> External Secrets Operator (sync) -> Kubernetes Secret (consumed by pods). Secrets are never stored in git.
Pipeline
flowchart LR
Infisical["Infisical\nhomelab / prod"]
CSS["ClusterSecretStore\nUniversal Auth"]
ES["ExternalSecret\nper-service"]
K8s["K8s Secret\nin target namespace"]
Pod["Pod\nenv vars"]
Infisical --> CSS --> ES --> K8s --> Pod
Controls
| Control | Implementation |
|---|---|
| Storage | Infisical (self-hosted, in-cluster) |
| Sync mechanism | ESO with refreshInterval: 1h |
| Git protection | .gitignore excludes terraform.tfvars, *.tfstate, .env |
| Bootstrap credentials | Created by Terraform from terraform.tfvars (gitignored) |
| Rotation | Manual via Infisical UI, force-sync with ESO annotation |
| Per-service isolation | Each service has its own ExternalSecret and K8s Secret |
ExternalSecret Inventory
| ExternalSecret | Namespace | Keys |
|---|---|---|
openclaw-secret |
openclaw |
OPENCLAW_GATEWAY_TOKEN, OPENROUTER_API_KEY, GEMINI_API_KEY, GITHUB_TOKEN, DISCORD_BOT_TOKEN, DISCORD_WEBHOOK_DEUTSCH, DISCORD_WEBHOOK_ENGLISH, DISCORD_WEBHOOK_ALERTS, CURSOR_API_KEY |
authentik-secret |
authentik |
AUTHENTIK_SECRET_KEY, AUTHENTIK_BOOTSTRAP_PASSWORD, AUTHENTIK_BOOTSTRAP_TOKEN, AUTHENTIK_POSTGRES_PASSWORD |
grafana-secret |
monitoring |
GRAFANA_ADMIN_PASSWORD, GRAFANA_OAUTH_CLIENT_SECRET |
Container Security
Non-Root Execution
| Service | runAsUser |
runAsNonRoot |
readOnlyRootFilesystem |
Notes |
|---|---|---|---|---|
| OpenClaw | 1000 | true |
no | Needs writable /data for workspaces |
| ArgoCD | Helm-managed | true |
true |
Restricted PSS compliant |
| ESO | Helm-managed | true |
true |
Restricted PSS compliant |
| Infisical | root | no | no | Upstream Helm default; hardening tracked in roadmap |
| Authentik | root | no | no | Upstream Helm default; hardening tracked in roadmap |
Image Provenance
| Service | Image | Source | Pinned |
|---|---|---|---|
| OpenClaw | openclaw:latest |
Built locally from submodule + Dockerfile.openclaw |
Submodule pinned to commit |
| ArgoCD | quay.io/argoproj/argocd |
Official upstream (Helm chart) | Chart version pinned |
| ESO | ghcr.io/external-secrets/external-secrets |
Official upstream (Helm chart) | Chart version pinned |
| Prometheus/Grafana | kube-prometheus-stack images | Official upstream (Helm chart) | Chart version pinned |
| Trivy | aquasecurity/trivy |
Official upstream (Helm chart) | Chart version pinned |
Supply Chain Security
| Control | Status | Details |
|---|---|---|
Branch protection on main |
Enforced | PRs require review; no direct push; linear history required |
| ArgoCD repo access (HTTPS) | Active | Unauthenticated HTTPS clone of public repo — no credentials stored |
| ArgoCD self-heal | Enabled | Manual kubectl changes are reverted within ~3 minutes |
| Helm chart version pinning | Enforced | All Application CRs pin targetRevision |
| Container image scanning | Active | Trivy Operator (ClientServer mode) scans all running images |
| Signed commits | Not enforced | Tracked in hardening roadmap |
| Dependabot / Renovate | Not configured | Tracked in hardening roadmap |
| Image signing (cosign) | Not configured | Tracked in hardening roadmap |
Vulnerability Scanning
Trivy Operator runs in ClientServer mode in the monitoring namespace. A dedicated trivy-server StatefulSet maintains the vulnerability database on a persistent volume. Scan jobs query it over HTTP instead of each managing their own cache.
| Setting | Value |
|---|---|
| Mode | ClientServer (centralized DB) |
| Concurrent scan jobs | 3 |
| Excluded namespaces | openclaw (locally-built image not in any registry) |
| Report type | VulnerabilityReport CRs per pod |
# List all vulnerability reports
kubectl get vulnerabilityreports -A
# View critical/high vulnerabilities
kubectl get vulnerabilityreports -A -o json | \
jq '.items[] | select(.report.summary.criticalCount > 0 or .report.summary.highCount > 0) |
{namespace: .metadata.namespace, name: .metadata.name, critical: .report.summary.criticalCount, high: .report.summary.highCount}'
LLM / AI Agent Security (OpenClaw)
This section provides a detailed breakdown of OpenClaw's permissions, access, and blast radius. OpenClaw is a multi-agent AI gateway running in the openclaw namespace that can execute tools (kubectl, git, gh) on behalf of AI-driven agents.
Kubernetes RBAC
The openclaw ServiceAccount has two layers of RBAC:
- Namespace Role (
openclaw-roleinopenclawnamespace) — namespace-specific permissions - ClusterRole (
openclaw-homelab-admin) — cluster-wide read + targeted operational writes
Namespace Role (openclaw namespace only)
| Resources | Verbs | Risk |
|---|---|---|
| secrets | get, list, watch | Medium — can read secrets in the openclaw namespace (API keys) |
| pods/exec | create | Medium — allows shell access into pods in the openclaw namespace |
Since only the OpenClaw pod itself runs in the namespace, pods/exec is effectively self-referential. Secrets read is limited to the openclaw namespace — the agent cannot read secrets in argocd, monitoring, authentik, or other namespaces.
ClusterRole (cluster-wide)
| Resources | Verbs | Purpose | Risk |
|---|---|---|---|
| pods, pods/log, services, endpoints, configmaps, events, nodes, namespaces, PVCs, PVs, replicationcontrollers | get, list, watch | Cluster-wide monitoring (kubectl get pods -A, kubectl describe node) |
Low (read-only) |
| deployments, statefulsets, daemonsets, replicasets | get, list, watch | Workload status across namespaces | Low (read-only) |
| deployments, statefulsets | patch | Rollout restart (annotation), rollout undo | Medium — can restart or roll back any deployment |
| deployments/scale, statefulsets/scale | get, patch | Emergency scaling (kubectl scale) |
Medium — can scale to 0 (mitigated by Critical Risk Protocol) |
| pods | delete | Remove stuck pods | Low |
| configmaps, services, PVCs | patch | Annotate resources for troubleshooting | Low |
| externalsecrets (external-secrets.io) | get, list, watch, patch | Check sync status, force-sync annotation | Low |
| applications, appprojects (argoproj.io) | get, list, watch | Check ArgoCD sync status | Low (read-only) |
| applications (argoproj.io) | patch | Hard refresh annotation | Low |
| pods, nodes (metrics.k8s.io) | get, list | kubectl top pods/nodes |
Low (read-only) |
What the ClusterRole does NOT grant
| Capability | Why excluded |
|---|---|
| Create deployments, services, namespaces, configmaps | Persistent resource creation goes through GitOps (git → ArgoCD) |
| Delete deployments, services, statefulsets, namespaces | Destructive deletions go through GitOps (git revert → ArgoCD prune) |
Read secrets outside openclaw namespace |
Secrets in argocd, monitoring, authentik, infisical are not accessible |
| Modify ClusterRoles, ClusterRoleBindings, NetworkPolicies | Security-sensitive cluster resources are GitOps-only |
| Create/delete ArgoCD Applications or AppProjects | Application lifecycle is managed through git manifests |
| Exec into pods in other namespaces | pods/exec is namespace-scoped to openclaw only |
This design allows the agent to monitor everything, operate on running workloads, but never create or destroy infrastructure. All persistent changes flow through GitOps.
Secrets Accessible
Eight secrets are injected as environment variables into the OpenClaw container:
| Secret Key | Purpose | Scope |
|---|---|---|
OPENCLAW_GATEWAY_TOKEN |
Gateway authentication (device pairing + API access) | OpenClaw gateway only |
OPENROUTER_API_KEY |
LLM inference via OpenRouter | OpenRouter account (usage-based billing) |
GEMINI_API_KEY |
LLM inference via Google Gemini (fallback) | Google AI Studio account |
GITHUB_TOKEN |
GitHub API access for the agent git workflow | Fine-grained PAT scoped to holdennguyen/homelab only: read access to metadata; read and write access to code, issues, and pull requests |
DISCORD_BOT_TOKEN |
Discord bot for chat-based agent interaction | Discord application bot user |
DISCORD_WEBHOOK_DEUTSCH |
Discord webhook for German learning reminders | Single Discord channel |
DISCORD_WEBHOOK_ENGLISH |
Discord webhook for IELTS learning reminders | Single Discord channel |
CURSOR_API_KEY |
Cursor CLI authentication for headless code generation | Cursor account (AI-assisted coding) |
The GITHUB_TOKEN is the most sensitive credential from a blast-radius perspective. Its scope is intentionally narrow:
- Limited to a single repository (
holdennguyen/homelab) - Cannot access other repositories, organizations, or GitHub settings
- Cannot manage repository settings, webhooks, or deploy keys
- Write access is limited to code (branches/PRs), issues, and pull requests
All eight secrets are also readable via kubectl get secret openclaw-secret -n openclaw due to the RBAC secrets read permission. Any process running inside the container can access them through the environment.
Host Filesystem Access
Two hostPath volumes are mounted into the pod:
| Volume | Host Path | Mount Path | Mode |
|---|---|---|---|
workspace-src |
/Users/holden.nguyen/homelab/agents/workspaces |
/workspace-src |
Read-only |
openclaw-skills |
/Users/holden.nguyen/homelab/skills |
/skills |
Read-only |
Both mounts are read-only. The exposed paths contain only agent personality Markdown files and skill definitions — no credentials, no broader filesystem access.
Container Security Context
securityContext:
runAsUser: 1000
runAsGroup: 1000
runAsNonRoot: true
fsGroup: 1000
| Control | Status |
|---|---|
| Non-root execution | Enforced (UID 1000) |
runAsNonRoot |
true |
allowPrivilegeEscalation |
Not set (defaults to true) |
capabilities.drop: [ALL] |
Not set |
readOnlyRootFilesystem |
Not set (needs writable /data) |
seccompProfile |
Not set |
Missing controls are tracked in the Hardening Roadmap.
Network Exposure
| Layer | Port | Access |
|---|---|---|
| Container | 18789 | Pod-internal |
| NodePort | 30789 | localhost only (OrbStack) |
| Tailscale Serve | 8447 | https://holdens-mac-mini.story-larch.ts.net:8447 (tailnet only) |
Authentication layers:
- Tailscale identity — only devices on the tailnet (WireGuard-authenticated) can reach the endpoint
- Gateway token — clients must present
OPENCLAW_GATEWAY_TOKENto authenticate - Device pairing — remote connections require explicit one-time approval via
devices approve
The gateway starts with --allow-unconfigured, which permits connections from any client that presents a valid token (no pre-registration required). The trustedProxies setting covers all RFC 1918 ranges (192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12).
NetworkPolicy rules for openclaw namespace:
- Default-deny all ingress and egress
- Allow intra-namespace communication
- Allow DNS to
kube-system - Allow ingress from Tailscale CIDR (
100.64.0.0/10) on port 18789 - Allow egress to Kubernetes API server on port 6443
- Allow egress to internet on port 443 (HTTPS — required for LLM API calls and GitHub)
Agent Capabilities
OpenClaw runs six agents in a two-tier orchestrator pattern (1 orchestrator, 1 senior lead, 4 junior agents):
| Agent | Skills | Can Delegate To |
|---|---|---|
homelab-admin (orchestrator) |
homelab-admin, gitops, secret-management, incident-response, weather, deutsch-tutor, english-tutor | cursor-agent, devops-sre, software-engineer, security-analyst, qa-tester, deutsch-tutor, english-tutor |
cursor-agent (senior lead) |
cursor-agent, gitops, software-engineer, security-analyst, qa-tester | devops-sre, software-engineer, security-analyst, qa-tester |
devops-sre (junior) |
devops-sre, gitops, secret-management, incident-response | — |
software-engineer (junior) |
software-engineer, gitops | — |
security-analyst (junior) |
security-analyst, gitops, secret-management | — |
qa-tester (junior) |
qa-tester, gitops, secret-management, incident-response | — |
Spawning limits:
| Setting | Value |
|---|---|
maxSpawnDepth |
2 (orchestrator -> sub-agent -> leaf worker) |
maxConcurrent |
4 parallel sub-agents |
maxChildrenPerAgent |
3 per session |
archiveAfterMinutes |
120 (auto-cleanup) |
sessions.visibility |
all (every agent can see every session) |
Tools available inside the container:
| Tool | Capability |
|---|---|
kubectl |
Kubernetes operations (limited by RBAC to openclaw namespace) |
helm |
Helm chart operations |
terraform |
Terraform commands (no state file in pod — informational only) |
argocd |
ArgoCD CLI |
git |
Git operations (authenticated via GITHUB_TOKEN through gh auth git-credential) |
gh |
GitHub CLI (create issues, PRs, manage labels — scoped to holdennguyen/homelab) |
jq |
JSON processing |
Agent Git Workflow Guardrails
Agents interact with the GitHub repository through a controlled workflow:
flowchart LR
Agent["AI Agent"]
Branch["Feature Branch\nagent-id/type/N-desc"]
PR["Pull Request\nlabeled + signed"]
Review["Human Review\n(required)"]
Main["main branch\n(protected)"]
ArgoCD["ArgoCD\nauto-sync"]
Agent --> Branch --> PR --> Review --> Main --> ArgoCD
| Guardrail | Implementation |
|---|---|
No direct push to main |
GitHub branch protection — PRs require at least one approving review |
| Agent identity tracing | Every commit, branch, issue, and PR carries the agent ID (author, suffix, labels, footer) |
| Scoped GitHub token | Fine-grained PAT: only holdennguyen/homelab, only code/issues/PRs |
| ArgoCD self-heal | Any manual kubectl apply is reverted within ~3 minutes |
| Human approval gate | All PRs require explicit human review before merge |
Risk Summary
| # | Risk | Severity | Current Mitigation | Residual Risk |
|---|---|---|---|---|
| 1 | GITHUB_TOKEN grants repo write access to all agents |
Medium | Scoped to single repo; PR review required; agent footprint tracing | A misbehaving agent could create spam PRs or issues |
| 2 | pods/exec + secrets read lets agents read their own secrets via kubectl |
Medium | Only one pod in namespace; secrets read is namespace-scoped to openclaw |
If more pods are added to openclaw ns, blast radius grows |
| 3 | ClusterRole allows patch on deployments/statefulsets cluster-wide |
Medium | Limited to patch (restart/scale) — cannot create or delete workloads; Critical Risk Protocol requires user confirmation for scaling to 0 | A misbehaving agent could restart or scale down any deployment |
| 4 | ClusterRole allows patch on ArgoCD Applications |
Low | Limited to patch (hard refresh annotation) — cannot create or delete Applications | Agent could trigger unnecessary refreshes; self-heal reverts any drift |
| 5 | Missing allowPrivilegeEscalation: false |
Low | Container runs as non-root (UID 1000) | Theoretical escalation path if kernel vulnerability exists |
| 6 | --allow-unconfigured on gateway |
Low | Tailscale + gateway token + device pairing provide three auth layers | Removes one defense layer (client pre-registration) |
| 7 | trustedProxies covers all RFC 1918 space |
Low | Only OrbStack internal traffic uses these ranges | Broader than necessary; could be narrowed |
| 8 | sessions.visibility: "all" |
Low | Single-user system; no multi-tenant data | All agents see all session history |
| 9 | Read-only hostPath volumes expose host filesystem | Low | Paths are narrow; read-only; contain only Markdown files | Container compromise could read skill/agent definitions |
| 10 | LLM API keys grant inference access | Low | Keys are per-provider; usage-based billing with spending limits | Compromised key could incur API costs |
OpenClaw Hardening Recommendations
- Add
allowPrivilegeEscalation: falseandcapabilities.drop: [ALL]to the container securityContext - Add
seccompProfile: { type: RuntimeDefault }to the pod securityContext - Narrow
trustedProxiesto the actual OrbStack pod CIDR instead of all RFC 1918 ranges - Consider removing
--allow-unconfiguredif all client devices are known - Consider per-agent GitHub tokens with even narrower scopes if agent count grows
- Restrict
sessions.visibilityif sensitive data flows through agent sessions
Host Isolation — How OpenClaw Cannot Reach the Mac Mini
OpenClaw runs inside a Kubernetes container on OrbStack. Multiple independent layers prevent it from accessing the bare-metal Mac mini, personal files, macOS credentials, or anything outside its narrow sandbox.
Isolation layers (defense in depth)
flowchart TD
subgraph host["Mac mini M4 (macOS)"]
FS["macOS filesystem\n~/Documents, ~/Desktop,\nKeychain, Safari passwords,\niMessage DB, Photos, etc."]
Procs["macOS processes\nFinder, Mail, Messages,\nKeychain Access, etc."]
Net["macOS network stack\nWi-Fi, Bluetooth, Tailscale"]
end
subgraph orbstack["OrbStack VM (Linux)"]
subgraph k8s["Kubernetes Cluster"]
subgraph openclawNs["openclaw namespace"]
Pod["OpenClaw Pod\nUID 1000, non-root"]
end
end
end
Pod -. "BLOCKED\nno access" .-> FS
Pod -. "BLOCKED\nno access" .-> Procs
Pod -- "only HTTPS :443\n(LLM APIs, GitHub)" --> Net
Layer-by-layer breakdown
| Layer | What it does | What OpenClaw cannot do |
|---|---|---|
| OrbStack VM boundary | OrbStack runs Kubernetes inside a lightweight Linux VM. The container's Linux kernel is separate from macOS. | Cannot invoke macOS APIs, cannot access macOS processes, cannot use macOS Keychain, cannot interact with Finder/Messages/Safari |
| Container filesystem isolation | The container has its own root filesystem. Only explicitly mounted volumes are visible. | Cannot see /Users/holden.nguyen/, cannot read Desktop/Documents/Downloads, cannot access Photos/Music/Mail, cannot read browser profiles or cookies |
| hostPath scope | Two host directories are mounted — both read-only, both containing only Markdown files | Can read agents/workspaces/*.md and skills/*.md. Cannot write to them. Cannot mount additional host paths without changing the deployment manifest (which requires a PR + human review) |
| Non-root execution | Pod runs as UID 1000 with runAsNonRoot: true |
Cannot escalate to root inside the container, cannot modify system binaries, cannot change container network config |
| Targeted RBAC (no cluster-admin) | ClusterRole grants cluster-wide read + scoped operational writes (restart, scale, annotate). Namespace Role grants secrets read + pods/exec in openclaw only |
Cannot create or delete deployments, services, or namespaces. Cannot read secrets outside openclaw. Cannot modify ClusterRoles, NetworkPolicies, or RBAC resources. Cannot exec into pods in other namespaces |
| Network policies | Default-deny with explicit allowlist: DNS, K8s API (:6443), Tailscale ingress (:18789), internet egress (:443) | Cannot reach other namespaces' pods over the network (declarative intent — enforcement depends on CNI). Cannot open arbitrary ports. Cannot reach macOS services on the host network (except through the K8s API server) |
| Secret scoping | Only openclaw-secret is injected (8 keys). Infisical stores all other secrets in separate ExternalSecrets per namespace |
Cannot read Authentik passwords, Grafana credentials, PostgreSQL passwords, or any secret outside its namespace |
| GitHub token scope | Fine-grained PAT: only holdennguyen/homelab, only code/issues/PRs |
Cannot access other repos, cannot modify repo settings/webhooks, cannot access GitHub account settings, cannot read private repos beyond homelab |
| Git workflow guardrails | Branch protection on main requires PR + human review |
Cannot push directly to main, cannot merge without human approval, cannot bypass branch protection |
What OpenClaw CAN access
To be precise about the actual attack surface:
| Resource | Access level | Risk |
|---|---|---|
Its own container filesystem (/data, /tmp, etc.) |
Read-write | Low — only agent workspace data, no personal files |
agents/workspaces/*.md via hostPath |
Read-only | Minimal — only Markdown personality files |
skills/*.md via hostPath |
Read-only | Minimal — only skill definition Markdown files |
openclaw-secret (8 keys) |
Read via env vars and kubectl get secret |
Medium — LLM API keys could incur billing; GitHub token scoped to one repo; Cursor API key grants AI code generation access |
Pods in openclaw namespace |
Read + exec | Medium — only the OpenClaw pod itself runs there |
| Pods, deployments, services, events in all namespaces | Read-only | Low — monitoring only, cannot modify |
| Deployments, statefulsets in all namespaces | Patch (restart, scale) | Medium — can restart or scale workloads; scaling to 0 is gated by Critical Risk Protocol |
| ArgoCD Applications | Read + patch (hard refresh) | Low — can trigger syncs but cannot create/delete apps |
| ExternalSecrets in all namespaces | Read + patch (force-sync) | Low — can check status and trigger re-sync |
Secrets outside openclaw namespace |
No access | N/A — cannot read secrets in argocd, monitoring, authentik, infisical |
| Internet on port 443 | Outbound HTTPS | Required for LLM APIs and GitHub; could theoretically exfiltrate data visible to the agent |
What would need to change for OpenClaw to harm the host
For OpenClaw to access personal files, macOS credentials, or bare-metal resources, an attacker would need to:
- Escape the container — exploit a Linux kernel vulnerability in the OrbStack VM to break out of the container namespace
- Escape the VM — exploit the OrbStack hypervisor to break from the Linux VM into macOS
- Escalate on macOS — gain access to the logged-in user's macOS session
Each of these is a distinct, independently difficult exploit. The combination of all three makes host compromise via OpenClaw extremely unlikely in practice — especially for a single-user homelab that is not exposed to the public internet.
Personal Task Gateway — Safely Extending OpenClaw to Personal Tasks
If you want OpenClaw to handle personal tasks — sending messages, making phone calls, checking bank balances — you should never give it direct access to the Mac mini. Instead, build a Kubernetes-native gateway layer that brokers access to each personal service through constrained, auditable APIs.
Architecture: K8s proxy services instead of host access
flowchart TD
subgraph openclawNs["openclaw namespace"]
OC["OpenClaw Pod"]
end
subgraph gatewayNs["personal-gateway namespace"]
MsgGW["messaging-proxy\n(iMessage/SMS API)"]
CallGW["voice-proxy\n(phone call API)"]
BankGW["finance-proxy\n(bank read-only API)"]
AuditLog["audit-logger\n(all actions logged)"]
end
subgraph host["Mac mini (macOS)"]
Shortcuts["Shortcuts.app\n(sandboxed automation)"]
Keychain["macOS Keychain\n(credentials stay here)"]
end
subgraph external["External APIs"]
Twilio["Twilio / VoIP"]
Plaid["Plaid / bank API"]
end
OC -- "K8s Service\nstructured API" --> MsgGW
OC -- "K8s Service\nstructured API" --> CallGW
OC -- "K8s Service\nstructured API" --> BankGW
MsgGW --> Shortcuts
CallGW --> Twilio
BankGW --> Plaid
MsgGW & CallGW & BankGW --> AuditLog
Design principles
| Principle | Why | How |
|---|---|---|
| No host shell access | A shell on the Mac mini means access to everything — Keychain, iMessage DB, browser sessions, all files | Every personal capability is a dedicated K8s service (a proxy) that exposes a narrow, typed API — never a shell |
| Least-privilege per service | Banking does not need message access; messaging does not need call access | Each proxy runs in its own pod with its own ServiceAccount, secrets, and NetworkPolicy. OpenClaw talks to each via K8s Service DNS |
| Audit everything | Personal actions (send a message, initiate a call, check a balance) must be logged for review | Every proxy logs the request, who triggered it, the timestamp, and the outcome. Logs are retained on a PVC and queryable |
| Read-only by default | Checking a bank balance is safe; initiating a wire transfer is not | Proxies default to read-only. Write actions (send message, make call, initiate payment) require explicit confirmation via a callback to the user |
| Credentials stay on the host | Bank passwords, iMessage auth, and phone account credentials must never enter a K8s pod | The proxy calls out to the host (e.g., via macOS Shortcuts HTTP endpoint or a host-side daemon) which holds the actual credentials in Keychain |
| Separate namespace | Isolate personal task infrastructure from homelab infrastructure | All proxies run in a dedicated personal-gateway namespace with its own RBAC, secrets, and network policies |
Per-capability architecture
Messaging (iMessage / SMS)
| Component | Where | Role |
|---|---|---|
messaging-proxy pod |
personal-gateway namespace |
Accepts structured API calls (send_message, read_messages) from OpenClaw. Validates, rate-limits, and logs |
| Host-side daemon or Shortcuts endpoint | Mac mini macOS | Receives HTTP requests from the proxy (via NodePort/localhost). Interacts with Messages.app via AppleScript or Shortcuts. Credentials never leave the host |
| NetworkPolicy | K8s | messaging-proxy can only talk to the host on one specific port. OpenClaw can only talk to messaging-proxy |
API contract example:
POST /api/messages/send
{
"to": "+1234567890",
"body": "Running 10 minutes late",
"confirmation_required": true
}
Response: { "status": "pending_confirmation", "confirmation_id": "abc123" }
The proxy never sends the message directly — it returns a confirmation ID. OpenClaw must relay this to the user. Only after the user confirms does the proxy forward the request to the host.
Phone calls (VoIP)
| Component | Where | Role |
|---|---|---|
voice-proxy pod |
personal-gateway namespace |
Accepts call requests, validates the number against an allowlist, logs, and forwards to Twilio/VoIP provider |
| Twilio API | External | Initiates the actual call. Credentials (Twilio SID, auth token) are in Infisical, injected via ESO into the personal-gateway namespace only |
| Rate limiter | In voice-proxy |
Max N calls per hour, no international numbers unless explicitly allowlisted |
Safeguards:
- Phone number allowlist (only pre-approved contacts)
- Call duration limit
- No simultaneous calls
- All calls logged with timestamp, number, duration, and who triggered it
Banking / finance (read-only)
| Component | Where | Role |
|---|---|---|
finance-proxy pod |
personal-gateway namespace |
Accepts read-only queries (get_balance, list_transactions). No write operations |
| Plaid API (or similar) | External | Provides read-only bank account access via tokenized OAuth. The Plaid access token is in Infisical, never in the pod env |
| Write operations | Intentionally absent | The proxy does not implement transfer, pay, or any write endpoint. There is no code path for moving money, so even a compromised agent cannot initiate a transaction |
Safeguards:
- Strictly read-only API (no transfer/payment endpoints exist)
- Plaid access token scoped to read-only permissions at the provider level
- Response data filtered: full account numbers are never returned, only last-4 digits
- Query rate limited: max N requests per hour
Kubernetes controls for the personal-gateway namespace
# Namespace with restricted PSS
apiVersion: v1
kind: Namespace
metadata:
name: personal-gateway
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
| Control | Configuration |
|---|---|
| RBAC | Each proxy pod gets its own ServiceAccount with zero K8s API permissions (no Role/RoleBinding). They are pure application pods — they do not need kubectl access |
| NetworkPolicy | Default-deny all. Allow ingress from openclaw namespace only (on specific proxy ports). Allow egress only to the specific external API each proxy needs. No cross-proxy communication |
| Secrets | Each proxy has its own ExternalSecret pulling only its specific credentials from Infisical. The messaging proxy cannot read the banking token and vice versa |
| Pod Security | restricted PSS enforced: non-root, read-only root filesystem, no privilege escalation, no host access, seccomp profile |
| Resource limits | CPU and memory limits prevent a compromised proxy from consuming node resources |
| No hostPath | Personal-gateway pods do NOT mount any host directories. All host interaction goes through network calls to a host-side daemon |
Network flow diagram
flowchart LR
subgraph openclawNs["openclaw namespace"]
OC["OpenClaw"]
end
subgraph gatewayNs["personal-gateway namespace"]
MP["messaging-proxy\nport 8081"]
VP["voice-proxy\nport 8082"]
FP["finance-proxy\nport 8083"]
end
subgraph host["Mac mini host"]
HD["host-daemon\nport 9090\nlocalhost only"]
end
subgraph internet["External APIs"]
TW["Twilio API\n:443"]
PL["Plaid API\n:443"]
end
OC -- "allowed by NP" --> MP
OC -- "allowed by NP" --> VP
OC -- "allowed by NP" --> FP
MP -- "NodePort → localhost" --> HD
VP -- ":443 only" --> TW
FP -- ":443 only" --> PL
MP -. "BLOCKED" .-> TW
MP -. "BLOCKED" .-> PL
FP -. "BLOCKED" .-> TW
FP -. "BLOCKED" .-> HD
OC -. "BLOCKED" .-> HD
Each proxy can only reach its designated backend. OpenClaw cannot bypass the proxies to reach the host daemon or external APIs directly (enforced by NetworkPolicy).
Summary: full power vs. K8s-mediated access
| Approach | OpenClaw gets | Risk |
|---|---|---|
| Full host access (DO NOT DO THIS) | Shell on Mac mini, access to Keychain, iMessage DB, browser sessions, all files, all macOS APIs | Catastrophic — a misbehaving agent can read passwords, send messages as you, access bank accounts, delete files |
| K8s gateway layer (recommended) | Structured API calls to purpose-built proxies, each with its own credentials, audit log, rate limits, and confirmation gates | Controlled — each capability is independently scoped, auditable, and revocable. A compromised agent can only do what the proxy API permits, and write actions require human confirmation |
The K8s layer acts as a permission membrane: OpenClaw asks for things through typed APIs, but the actual credentials and execution happen in isolated, auditable proxies that the agent cannot modify or bypass.
Hardening Roadmap
Open items to improve the security posture in future releases:
| Item | Affected Services | Priority | Effort |
|---|---|---|---|
Add allowPrivilegeEscalation: false + capabilities.drop: [ALL] |
OpenClaw | High | Low |
Add seccompProfile: RuntimeDefault |
OpenClaw | High | Low |
Narrow OpenClaw trustedProxies to actual pod CIDR |
OpenClaw | Medium | Low |
Enforce restricted PSS on openclaw |
OpenClaw | Medium | Medium (requires replacing hostPath with a different volume strategy) |
| Harden Infisical and Authentik containers to non-root | Infisical, Authentik | Medium | High (depends on upstream chart support) |
| Enable GPG-signed commits for agent git operations | OpenClaw agents | Low | Medium |
| Configure Dependabot or Renovate for Helm chart updates | All Helm-deployed services | Low | Low |
| Image signing with cosign | OpenClaw (locally-built) | Low | Medium |
| Enforce NetworkPolicy at CNI level | All namespaces | Low | High (requires migration from OrbStack to Cilium/Calico CNI) |
Add Trivy scanning for openclaw namespace |
OpenClaw | Low | Low (push image to local registry) |