Compare commits

...

6 Commits

Author SHA1 Message Date
Andrew Stoltz
30e04a10c6 security(guacamole): scope guacd Kubernetes exec RBAC 2026-06-18 15:05:43 -05:00
Andrew Stoltz
fc24102fb9 docs(agent-zero): align kubectl tool text with SEC-6 RBAC 2026-06-18 14:47:04 -05:00
Andrew Stoltz
a77fbd0381 security(agent-zero): remove cluster RBAC entirely + no token mount (operator directive — MCP only)
Operator: agent-zero must reach the cluster ONLY through gated MCP tools, not a
service account with cluster roles for raw kubectl. Removed the read-only
ClusterRole/ClusterRoleBinding entirely (SA now has zero cluster perms) and set
automountServiceAccountToken: false so no K8s API token is mounted at all.
Applied live (SA secrets/exec/pods/namespaces -> all Forbidden); this makes it
durable so ArgoCD selfHeal won't re-create any role.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 14:46:07 -05:00
Andrew Stoltz
191eb91642 security(agent-zero): replace cluster-admin with least-privilege read-only RBAC (SEC-6/RBAC-001)
agent-zero is an LLM agent; cluster-admin let raw kubectl bypass the MCP layer to
read every Secret / exec any pod. Swap for a read-only ClusterRole (no secrets/
configmaps/exec/writes) so sensitive + mutating actions go through gated MCP tools.
Already applied live + verified (secrets/exec/write -> Forbidden, observe stays);
this makes it durable so ArgoCD selfHeal doesn't revert to cluster-admin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 14:46:07 -05:00
e4e03643a4 Merge PR #46: Harden UpdateCenter public route methods
P0 public-host containment: keep update.flowercore.io GET/HEAD only and preserve regression tests.
2026-06-18 15:38:59 +00:00
Andrew Stoltz
14195e5da7 harden updatecenter public route methods 2026-06-18 10:37:53 -05:00
7 changed files with 304 additions and 27 deletions

View File

@@ -75,19 +75,12 @@ metadata:
name: agent-zero
namespace: agent-zero
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: agent-zero-cluster-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: agent-zero
namespace: agent-zero
# SEC-6 / audit RBAC-001 (operator directive 2026-06-17): agent-zero is an LLM
# agent and must reach the cluster ONLY through gated MCP tools — NOT raw kubectl.
# It therefore has NO ClusterRole/ClusterRoleBinding at all, and the pod sets
# automountServiceAccountToken: false (below) so no Kubernetes API token is even
# mounted. History: cluster-admin -> read-only -> (now) no cluster RBAC / no token.
# Detail: FlowerCore.Notes docs/security/sec-6-agent-zero-rbac-remediation.md.
# =============================================================================
# Agent Zero — AI Agent Web UI (NUC Edition, Blue Jay Profile)
@@ -189,6 +182,9 @@ spec:
app: agent-zero
spec:
serviceAccountName: agent-zero
# SEC-6: no Kubernetes API token mounted — agent-zero reaches the cluster
# only via gated MCP tools, never raw kubectl. (RBAC is also removed above.)
automountServiceAccountToken: false
initContainers:
# Wait for fc-llm-bridge to be reachable before starting Agent Zero.
- name: wait-for-llm-bridge

View File

@@ -3616,7 +3616,8 @@ data:
kubectl_manager.py: |
# Kubernetes Cluster Management Tool
# Manages Kubernetes resources via kubectl on a Rancher Desktop (k3s) cluster.
# The pod runs with a cluster-admin ServiceAccount so all operations are permitted.
# SEC-6: the pod has no mounted Kubernetes API token and no cluster RBAC.
# Cluster operations must go through gated FlowerCore MCP tools instead.
# kubectl is located at /usr/local/bin/kubectl.
import subprocess
@@ -4442,7 +4443,8 @@ data:
### Notes
- The Agent Zero pod has `cluster-admin` privileges; all kubectl operations are permitted.
- The Agent Zero pod has no mounted Kubernetes API token and no cluster RBAC.
- Cluster operations must go through gated FlowerCore MCP tools instead of raw kubectl.
- kubectl is located at `/usr/local/bin/kubectl`.
- Long outputs are truncated to 4000 characters to avoid flooding.
- The `exec_command` action has a 30-second timeout.

View File

@@ -43,5 +43,6 @@ shared origin cert must exist in every namespace that serves a
```powershell
kubectl.exe --kubeconfig C:\Users\AndrewStoltz\.kube\rke2.yaml -n argocd get application infra-fc-updater
kubectl.exe --kubeconfig C:\Users\AndrewStoltz\.kube\rke2.yaml -n fc-updater get deploy,svc,ingressroute,certificate,pvc
curl.exe -sk https://update.flowercore.io/api/v1/manifests/_schema
curl.exe -sk https://update.flowercore.io/
curl.exe -sk -o NUL -w "%{http_code}`n" https://update.flowercore.io/login
```

View File

@@ -266,7 +266,7 @@ spec:
entryPoints:
- websecure
routes:
- match: (Host(`update.flowercore.io`) || Host(`updates.flowercore.io`)) && (Method(`GET`) || Method(`HEAD`) || Method(`POST`) || Method(`OPTIONS`))
- match: (Host(`update.flowercore.io`) || Host(`updates.flowercore.io`)) && (Method(`GET`) || Method(`HEAD`))
kind: Rule
services:
- name: updatecenter-web

View File

@@ -225,8 +225,7 @@ spec:
- "--port=8001"
- "--address=127.0.0.1"
- "--accept-hosts=.*"
- "--accept-paths=.*"
- "--disable-filter=true"
- "--accept-paths=^/api/v1/namespaces/(argocd|gitea|telephony|traefik-system|zabbix|matrix|irc|mail|selenium)/pods(/[^/]+(/(exec|attach))?)?$"
- "--v=2"
resources:
requests:
@@ -526,10 +525,13 @@ metadata:
name: guacd-exec
namespace: guacamole
---
# Namespace-scoped exec/list rights for the Kubernetes protocol and sync job.
# Keep this allowlist in lockstep with TARGET_NAMESPACES below.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
kind: Role
metadata:
name: guacd-pod-exec
namespace: argocd
labels:
app.kubernetes.io/component: proxy
app.kubernetes.io/name: guacd
@@ -540,20 +542,282 @@ rules:
- apiGroups: [""]
resources: ["pods/exec", "pods/attach"]
verbs: ["create", "get"]
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["list", "get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
kind: RoleBinding
metadata:
name: guacd-pod-exec
namespace: argocd
labels:
app.kubernetes.io/component: proxy
app.kubernetes.io/name: guacd
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
kind: Role
name: guacd-pod-exec
subjects:
- kind: ServiceAccount
name: guacd-exec
namespace: guacamole
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: guacd-pod-exec
namespace: gitea
labels:
app.kubernetes.io/component: proxy
app.kubernetes.io/name: guacd
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["pods/exec", "pods/attach"]
verbs: ["create", "get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: guacd-pod-exec
namespace: gitea
labels:
app.kubernetes.io/component: proxy
app.kubernetes.io/name: guacd
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: guacd-pod-exec
subjects:
- kind: ServiceAccount
name: guacd-exec
namespace: guacamole
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: guacd-pod-exec
namespace: telephony
labels:
app.kubernetes.io/component: proxy
app.kubernetes.io/name: guacd
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["pods/exec", "pods/attach"]
verbs: ["create", "get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: guacd-pod-exec
namespace: telephony
labels:
app.kubernetes.io/component: proxy
app.kubernetes.io/name: guacd
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: guacd-pod-exec
subjects:
- kind: ServiceAccount
name: guacd-exec
namespace: guacamole
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: guacd-pod-exec
namespace: traefik-system
labels:
app.kubernetes.io/component: proxy
app.kubernetes.io/name: guacd
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["pods/exec", "pods/attach"]
verbs: ["create", "get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: guacd-pod-exec
namespace: traefik-system
labels:
app.kubernetes.io/component: proxy
app.kubernetes.io/name: guacd
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: guacd-pod-exec
subjects:
- kind: ServiceAccount
name: guacd-exec
namespace: guacamole
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: guacd-pod-exec
namespace: zabbix
labels:
app.kubernetes.io/component: proxy
app.kubernetes.io/name: guacd
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["pods/exec", "pods/attach"]
verbs: ["create", "get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: guacd-pod-exec
namespace: zabbix
labels:
app.kubernetes.io/component: proxy
app.kubernetes.io/name: guacd
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: guacd-pod-exec
subjects:
- kind: ServiceAccount
name: guacd-exec
namespace: guacamole
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: guacd-pod-exec
namespace: matrix
labels:
app.kubernetes.io/component: proxy
app.kubernetes.io/name: guacd
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["pods/exec", "pods/attach"]
verbs: ["create", "get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: guacd-pod-exec
namespace: matrix
labels:
app.kubernetes.io/component: proxy
app.kubernetes.io/name: guacd
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: guacd-pod-exec
subjects:
- kind: ServiceAccount
name: guacd-exec
namespace: guacamole
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: guacd-pod-exec
namespace: irc
labels:
app.kubernetes.io/component: proxy
app.kubernetes.io/name: guacd
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["pods/exec", "pods/attach"]
verbs: ["create", "get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: guacd-pod-exec
namespace: irc
labels:
app.kubernetes.io/component: proxy
app.kubernetes.io/name: guacd
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: guacd-pod-exec
subjects:
- kind: ServiceAccount
name: guacd-exec
namespace: guacamole
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: guacd-pod-exec
namespace: mail
labels:
app.kubernetes.io/component: proxy
app.kubernetes.io/name: guacd
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["pods/exec", "pods/attach"]
verbs: ["create", "get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: guacd-pod-exec
namespace: mail
labels:
app.kubernetes.io/component: proxy
app.kubernetes.io/name: guacd
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: guacd-pod-exec
subjects:
- kind: ServiceAccount
name: guacd-exec
namespace: guacamole
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: guacd-pod-exec
namespace: selenium
labels:
app.kubernetes.io/component: proxy
app.kubernetes.io/name: guacd
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["pods/exec", "pods/attach"]
verbs: ["create", "get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: guacd-pod-exec
namespace: selenium
labels:
app.kubernetes.io/component: proxy
app.kubernetes.io/name: guacd
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: guacd-pod-exec
subjects:
- kind: ServiceAccount

View File

@@ -1013,6 +1013,22 @@ public sealed class FleetManifestLintTests
match.Should().NotContain("Method(`POST`)");
}
[Fact]
public void UpdateCenterPublicIngress_KeepsDeliveryOnlyGetHeadMethodAllowlist()
{
var publicIngress = AppDocuments("fc-updater")
.Single(document => document.Kind == "IngressRoute" && document.Name == "updatecenter-web-public");
var route = publicIngress.MappingSequence("spec", "routes").Should().ContainSingle().Subject;
var match = ManifestNodeExtensions.Scalar(route, "match");
match.Should().Contain("Host(`update.flowercore.io`)");
match.Should().Contain("Host(`updates.flowercore.io`)");
match.Should().Contain("Method(`GET`)");
match.Should().Contain("Method(`HEAD`)");
match.Should().NotContain("Method(`POST`)");
match.Should().NotContain("Method(`OPTIONS`)");
}
[Fact]
public void DnsAndMediaIngressRoutes_MatchLiveInternalHosts()
{

View File

@@ -9,8 +9,6 @@ package bluejayinfra.public_readwrite_allowlist
public_readwrite_hosts := {
"updatecenter.iamworkin.lan",
"updates.iamworkin.lan",
"update.flowercore.io",
"updates.flowercore.io",
}
required_methods := {"GET", "HEAD", "POST", "OPTIONS"}