Skip to content

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

WhereSet withDecides
Management clusteronboarding.admins in the hub chartWho may add, edit and delete clusters in the Add cluster wizard (Add clusters)
Each workload clusteruserRBAC in the agent chartWho 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

  1. 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:alice and the groups eddy:platform and eddy:authenticated.
  2. 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.
  3. 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.
  4. The agent refuses system:* users and groups, users matching denyUserPrefixes, and any group outside allowedGroupPrefixes (default eddy:). 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
RoleBound to (default)What it allows
eddy-vieweruserRBAC.viewer.groups: eddy:authenticated, every signed-in userget, list and watch on every kind the agent watches (preset kinds only when enabled), plus get on pods/log. Never Secrets or ConfigMaps
eddy-operatoruserRBAC.operator.groups: none until you set themEverything 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.namespaces creates RoleBindings in those namespaces instead of cluster-wide bindings.
  • Extra bindings. userRBAC.bindings takes entries with a name, a role (viewer or operator), groups, users and optional namespaces. For example, an operator group for one cluster only:
    userRBAC:bindings:- name: prod-eu-operatorsrole: operatorgroups: ["eddy:operators:prod-eu"]
  • userRBAC.roleNames renames the roles and userRBAC.extraRules adds rules to them.
  • Logs are readable only with get on pods/log. To keep logs out of Eddy, set userRBAC.create: false and apply your own copy of deploy/rbac/eddy-user-rbac.yaml without the pods/log rule.
  • Secrets and ConfigMaps are not in the roles. Eddy never shows Secret YAML and strips data from 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
IdentityKubernetes group to bind
Local user with groups: [platform]eddy:platform (the user is local:<name>)
GitHub organizationeddy:github:acme
GitHub teameddy:github:acme/platform
Okta or Dex group eddy-sreeddy:eddy-sre
Entra ID groupeddy:<group object id>
Every signed-in usereddy: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

ActionViewerOperator
See clusters, lists, counts, graph, YAML, eventsYesYes
Read pod and workload logsYes (needs pods/log)Yes
Reconcile, reconcile with sourceNoYes
Suspend and resumeNoYes
Add, edit or delete a cluster in EddyNeeds onboarding.admins in the hub chart (RBAC on clusters.gitops.eddy.dev in the management cluster), see Add clusters
Threads, Ask AI, access tokensAny 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

  1. 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
  2. Sign in as a user from a bound group. The cluster should list its resources. A user with no binding sees an empty list.
  3. Open /api/v1/me to 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 patch on that kind. Bind them to eddy-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.