Access and RBAC
Eddy has no permission model of its own: ordinary Kubernetes RBAC in each workload cluster decides. By default the agent chart lets every signed-in user read; you decide who may operate. This page explains how identities become Kubernetes subjects and which roles to grant.
Full reference: deploy/rbac/eddy-user-rbac.yaml and the eddy-agent chart values in the repository.
Two clusters, two kinds of roles
| Where | Set with | Decides |
|---|---|---|
| Management cluster | onboarding.admins in the hub chart | Who may add, edit and delete clusters in the Add cluster wizard (Add clusters) |
| Each workload cluster | userRBAC in the agent chart | Who may read and act on Flux and workloads there (this page) |
Ask AI and MCP need no roles of their own: they run as the asking user. The AWS IAM role for Bedrock in Ask AI is an AWS credential for the hub pod, not Kubernetes RBAC.
How impersonation works
- A person signs in to the hub. The hub turns the identity into a Kubernetes user and groups (see group mapping): for example the user
local:aliceand the groupseddy:platformandeddy:authenticated. - For every direct read (YAML, events, logs) and every write, the hub asks the agent to call the Kubernetes API with impersonation headers for that user and those groups. The API server's RBAC decides, and the cluster's own audit log records the real user.
- Lists and counts come from the agent's shared cache. The hub filters them per user with SubjectAccessReviews (cached for 45 seconds) before they leave the hub. This also covers thread lists, Ask AI and MCP results.
- The agent refuses
system:*users and groups, users matchingdenyUserPrefixes, and any group outsideallowedGroupPrefixes(defaulteddy:). The hub checks the same rules first.
So Eddy can never do more than the user could do with kubectl, and the agent's own ServiceAccount cannot write. A permission change takes effect within about 90 seconds.
1. Grant access with the agent chart
The eddy-agent chart creates two ClusterRoles, eddy-viewer and eddy-operator, and binds them to the groups you name. Set the groups in the agent values for each cluster:
userRBAC:create: true # the defaultviewer:groups: ["eddy:authenticated"] # the default: every signed-in user can readoperator:groups: ["eddy:github:acme/platform"] # who may reconcile, suspend and resume
| Role | Bound to (default) | What it allows |
|---|---|---|
eddy-viewer | userRBAC.viewer.groups: eddy:authenticated, every signed-in user | get, list and watch on every kind the agent watches (preset kinds only when enabled), plus get on pods/log. Never Secrets or ConfigMaps |
eddy-operator | userRBAC.operator.groups: none until you set them | Everything a viewer can do, plus patch on the Flux kinds, which is what reconcile, suspend and resume need |
By default every signed-in user can read summaries in that cluster. Set userRBAC.viewer.groups: [] to opt out and bind viewers yourself.
- Limit to namespaces. A list in
userRBAC.namespacescreates RoleBindings in those namespaces instead of cluster-wide bindings. - Extra bindings.
userRBAC.bindingstakes entries with aname, arole(vieweroroperator),groups,usersand optionalnamespaces. For example, an operator group for one cluster only:userRBAC:bindings:- name: prod-eu-operatorsrole: operatorgroups: ["eddy:operators:prod-eu"]
userRBAC.roleNamesrenames the roles anduserRBAC.extraRulesadds rules to them.- Logs are readable only with
getonpods/log. To keep logs out of Eddy, setuserRBAC.create: falseand apply your own copy ofdeploy/rbac/eddy-user-rbac.yamlwithout thepods/logrule. - Secrets and ConfigMaps are not in the roles. Eddy never shows Secret YAML and strips
datafrom ConfigMaps. Objects a Kustomization applied that Eddy does not watch appear as inventory-only rows (kind, namespace and name). A user sees one only if they can list its Kustomization and that kind in the namespace. - The Karpenter and External Secrets rules only matter when the agent enables the matching presets.
Without Helm: the example manifest
If you manage RBAC yourself, deploy/rbac/eddy-user-rbac.yaml in the repository holds the same two ClusterRoles and bindings (to eddy:authenticated and eddy:platform). Set userRBAC.create: false on the agent, apply the file in every workload cluster, and edit the subjects:
$ curl -fsSLO https://raw.githubusercontent.com/idestis/eddy/main/deploy/rbac/eddy-user-rbac.yaml$ kubectl --context prod-eu apply -f eddy-user-rbac.yaml
2. Know which group to bind
Bind groups, not individual users. A group name is the eddy: prefix plus what the identity provider sends. The same names work in userRBAC.*.groups and in your own RoleBindings, for example for a GitHub team:
apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata:name: eddy-operator-platformroleRef:apiGroup: rbac.authorization.k8s.iokind: ClusterRolename: eddy-operatorsubjects:- apiGroup: rbac.authorization.k8s.iokind: Groupname: eddy:github:acme/platform
| Identity | Kubernetes group to bind |
|---|---|
Local user with groups: [platform] | eddy:platform (the user is local:<name>) |
| GitHub organization | eddy:github:acme |
| GitHub team | eddy:github:acme/platform |
Okta or Dex group eddy-sre | eddy:eddy-sre |
| Entra ID group | eddy:<group object id> |
| Every signed-in user | eddy:authenticated |
Use userRBAC.namespaces, or a RoleBinding of your own, to limit a team to some namespaces. For Google or other providers that send no groups, map users to groups with config.auth.groups.static in the hub values.
3. Pin the agent's impersonation (recommended)
The agent may impersonate groups. To limit a compromised agent pod to the groups you actually use, list them in the agent chart. The chart then pins the impersonate rule on groups with resourceNames:
impersonation:groups: [eddy:authenticated, eddy:platform, eddy:github:acme/platform]
List every group you bind, or those users lose access. Users cannot be pinned the same way, so bind humans to groups, never to individual users with powerful roles.
Protected clusters: typed confirmation
A cluster marked protected: true (usually production) asks for a typed confirmation on every write: the UI asks the user to type the cluster name, and the hub checks it on the server. Over MCP the same check is the confirm_cluster argument. Operators can deny MCP writes to protected clusters entirely with config.mcp.protectedClusters: deny. The typed name is a speed bump: the real controls are RBAC and the audit log.
What each role can do
| Action | Viewer | Operator |
|---|---|---|
| See clusters, lists, counts, graph, YAML, events | Yes | Yes |
| Read pod and workload logs | Yes (needs pods/log) | Yes |
| Reconcile, reconcile with source | No | Yes |
| Suspend and resume | No | Yes |
| Add, edit or delete a cluster in Eddy | Needs onboarding.admins in the hub chart (RBAC on clusters.gitops.eddy.dev in the management cluster), see Add clusters | |
| Threads, Ask AI, access tokens | Any signed-in user, on the resources they can already see | |
The audit log is yours alone unless your group is in config.auth.auditViewerGroups (use the prefixed name, for example eddy:platform).
Verify
- Impersonate as a user yourself and check:
$ kubectl --context prod-eu auth can-i patch kustomizations.kustomize.toolkit.fluxcd.io \-n flux-system --as local:alice --as-group eddy:platform --as-group eddy:authenticated
- Sign in as a user from a bound group. The cluster should list its resources. A user with no binding sees an empty list.
- Open
/api/v1/meto see the groups the hub resolved for you.
Troubleshooting
- The UI is empty for a user. They have no RBAC in that cluster, or the group name in the binding does not match. Compare it with the groups in
/api/v1/me. - Reconcile returns Forbidden. The user lacks
patchon that kind. Bind them toeddy-operator. - A user lost access after you pinned groups. Their group is not in
impersonation.groups. Add it and upgrade the agent. - A team change did not apply. Groups are captured at sign-in. The user signs out and in again.