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

View File

@@ -3616,7 +3616,8 @@ data:
kubectl_manager.py: | kubectl_manager.py: |
# Kubernetes Cluster Management Tool # Kubernetes Cluster Management Tool
# Manages Kubernetes resources via kubectl on a Rancher Desktop (k3s) cluster. # 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. # kubectl is located at /usr/local/bin/kubectl.
import subprocess import subprocess
@@ -4442,7 +4443,8 @@ data:
### Notes ### 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`. - kubectl is located at `/usr/local/bin/kubectl`.
- Long outputs are truncated to 4000 characters to avoid flooding. - Long outputs are truncated to 4000 characters to avoid flooding.
- The `exec_command` action has a 30-second timeout. - 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 ```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 argocd get application infra-fc-updater
kubectl.exe --kubeconfig C:\Users\AndrewStoltz\.kube\rke2.yaml -n fc-updater get deploy,svc,ingressroute,certificate,pvc 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: entryPoints:
- websecure - websecure
routes: 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 kind: Rule
services: services:
- name: updatecenter-web - name: updatecenter-web

View File

@@ -225,8 +225,7 @@ spec:
- "--port=8001" - "--port=8001"
- "--address=127.0.0.1" - "--address=127.0.0.1"
- "--accept-hosts=.*" - "--accept-hosts=.*"
- "--accept-paths=.*" - "--accept-paths=^/api/v1/namespaces/(argocd|gitea|telephony|traefik-system|zabbix|matrix|irc|mail|selenium)/pods(/[^/]+(/(exec|attach))?)?$"
- "--disable-filter=true"
- "--v=2" - "--v=2"
resources: resources:
requests: requests:
@@ -526,10 +525,13 @@ metadata:
name: guacd-exec name: guacd-exec
namespace: guacamole 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 apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole kind: Role
metadata: metadata:
name: guacd-pod-exec name: guacd-pod-exec
namespace: argocd
labels: labels:
app.kubernetes.io/component: proxy app.kubernetes.io/component: proxy
app.kubernetes.io/name: guacd app.kubernetes.io/name: guacd
@@ -540,20 +542,282 @@ rules:
- apiGroups: [""] - apiGroups: [""]
resources: ["pods/exec", "pods/attach"] resources: ["pods/exec", "pods/attach"]
verbs: ["create", "get"] verbs: ["create", "get"]
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["list", "get"]
--- ---
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding kind: RoleBinding
metadata: metadata:
name: guacd-pod-exec name: guacd-pod-exec
namespace: argocd
labels: labels:
app.kubernetes.io/component: proxy app.kubernetes.io/component: proxy
app.kubernetes.io/name: guacd app.kubernetes.io/name: guacd
roleRef: roleRef:
apiGroup: rbac.authorization.k8s.io 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 name: guacd-pod-exec
subjects: subjects:
- kind: ServiceAccount - kind: ServiceAccount

View File

@@ -1013,6 +1013,22 @@ public sealed class FleetManifestLintTests
match.Should().NotContain("Method(`POST`)"); 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] [Fact]
public void DnsAndMediaIngressRoutes_MatchLiveInternalHosts() public void DnsAndMediaIngressRoutes_MatchLiveInternalHosts()
{ {

View File

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