Documentation
¶
Overview ¶
Package rbac analyzes Kubernetes and Docker identity configuration for over-privilege and privilege-escalation paths. It is a pure library: given a set of recorded API objects (RBAC roles/bindings, service accounts, pods) as JSON, plus an optional Docker-host descriptor, it builds a permission graph, answers reverse "who-can-X-on-Y" queries, and emits deterministic Risk records for the well-known escalation primitives (wildcards, escalate/bind/impersonate, secret reads, token minting, CSR signing, pod exec, workload creation, node proxy, cluster-admin, dangling bindings, default-SA usage, docker-group and socket exposure). It also reconstructs concrete pod → cluster-admin/node-root escalation chains and can generate least-privilege roles from observed usage.
The package never reads the wall clock or a random source: everything that needs "now" takes it through Options, so the same input always yields the same output (the golden test depends on this). The engine module in internal/modules/rbac projects these Risk values onto engine.Finding; the `dsecrat rbac` command renders them for humans.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Binding ¶
type Binding struct {
Name string
Namespace string // empty when ClusterScoped
ClusterScoped bool
Subjects []Subject
RoleRef RoleRef
}
Binding grants a RoleRef to a set of Subjects. RoleBinding is namespaced; ClusterRoleBinding (ClusterScoped) grants cluster-wide. A namespaced RoleBinding may still reference a ClusterRole — in which case the grant is scoped to the binding's namespace, which is a common source of confusion and mis-scoped access.
type CloudIdentity ¶
type CloudIdentity struct {
Provider CloudProvider
// Role is the provider-specific role reference: an IAM role ARN (AWS), a GCP
// service-account email, or an Azure client id.
Role string
// Privilege, when supplied by the input, describes the bound role's power so
// the analysis can flag over-privilege without querying the cloud. One of
// "admin", "power", "write", "read", or "" (unknown).
Privilege string
// TrustAnyServiceAccount is true when the role's trust policy does not pin the
// specific SA (subject condition) — a confused-deputy risk where any pod that
// can present a token from the cluster's OIDC issuer can assume the role.
TrustAnyServiceAccount bool
}
CloudIdentity is the cloud IAM role a Kubernetes ServiceAccount is bound to through workload-identity federation. Analyzing this bridge is what turns "who can do what in the cluster" into "what cloud blast radius a pod inherits".
type CloudProvider ¶
type CloudProvider string
CloudProvider names a cloud workload-identity mechanism.
const ( CloudAWS CloudProvider = "aws" // IRSA / EKS Pod Identity (eks.amazonaws.com/role-arn) CloudGCP CloudProvider = "gcp" // GKE Workload Identity (iam.gke.io/gcp-service-account) CloudAzure CloudProvider = "azure" // AKS Workload Identity (azure.workload.identity/client-id) )
type Cluster ¶
type Cluster struct {
Roles map[string]*Role // key: roleKey(clusterScoped, ns, name)
Bindings []*Binding
ServiceAccounts map[string]*ServiceAccount // key: ns/name
Pods []*Pod
DockerHosts []*DockerHost
// ObservedUsage maps a subject key to the permissions it was actually seen
// using (from an audit fixture). Drives least-privilege generation and the
// NHI dormant-identity check. Empty when no usage data was supplied.
ObservedUsage map[string][]Permission
}
Cluster is the whole parsed input: every object we will reason about. Maps are keyed for O(1) resolution during binding/role lookup; slices preserve nothing order-sensitive, so all analysis sorts its own output for determinism.
type DockerHost ¶
type DockerHost struct {
Name string
DockerGroupMembers []string
Rootless bool
SocketMounts []SocketMount
}
DockerHost captures the local-daemon identity surface. Membership of the docker group, or a mounted daemon socket, is effectively unaudited root — a classic non-Kubernetes privilege path we still want to catch.
type Grant ¶
type Grant struct {
Subject Subject
Role *Role
Binding *Binding
Namespace string // effective scope of the grant ("" = cluster-wide)
ClusterWide bool
}
Grant is a resolved binding: which subject got which role's rules, and in what scope. It is the intermediate the whole analysis is built on, and it records enough provenance (binding and role names) to explain any finding.
type Graph ¶
type Graph struct {
Grants []Grant
// contains filtered or unexported fields
}
Graph is the resolved RBAC universe: every grant, indexed for the reverse queries that make an access review tractable.
func (*Graph) SubjectPermissions ¶
func (g *Graph) SubjectPermissions(subjectKey string) []Permission
SubjectPermissions returns the flattened permission set for one subject key, expanding each granted role's rules into concrete Permission atoms. Wildcards are preserved as "*" rather than expanded against a resource catalog — we do not ship one, and for risk purposes "*" is more honest than a frozen list.
func (*Graph) WhoCan ¶
WhoCan returns every subject that can perform verb on resource (in apiGroup), optionally restricted to a namespace. This is the access-review workhorse: wildcards in a subject's grant match any query, which is exactly the danger wildcards represent. An empty namespace query matches grants in any scope.
type Options ¶
type Options struct {
// EnableNHI turns on the non-human-identity risk graph (AI-age feature). Off
// by default so the deterministic core never depends on the optional layer.
EnableNHI bool
// Now is the reference time for dormancy calculations. Zero means a fixed
// epoch, keeping analysis deterministic when no clock is injected.
Now time.Time
// DormantAfter is how long since last use before an identity is "dormant".
DormantAfter time.Duration
// BroadThreshold is the reachable-principal count above which a non-human
// identity is considered over-broad even without reaching a terminal target.
BroadThreshold int
}
Options tunes an analysis run. The zero value is a safe, deterministic default (NHI off, a fixed epoch "now", 90-day dormancy, blast-radius threshold 3), so callers can pass Options{} and get reproducible results.
type Permission ¶
type Permission struct {
Verb string
APIGroup string
Resource string
Namespace string // "" = cluster-wide
}
Permission is one concrete capability a subject holds: a verb on a resource in an apiGroup, scoped to a namespace ("" meaning cluster-wide / all namespaces). It is the atom of both the effective-permission set and observed-usage data.
func (Permission) String ¶
func (p Permission) String() string
String renders a permission compactly for output and least-privilege diffs, e.g. "get secrets.core in kube-system" or "* *.* cluster-wide".
type Pod ¶
type Pod struct {
Name string
Namespace string
ServiceAccountName string // empty means the namespace "default" SA
AutomountToken *bool
Privileged bool
HostPID bool
HostNetwork bool
HostPathMounts []string
AddedCapabilities []string
}
Pod is a workload just detailed enough to tie it to an identity and its escalation-relevant security context. Escalation analysis starts from pods, because a compromised pod is the usual patient zero.
type PolicyRule ¶
type PolicyRule struct {
APIGroups []string
Resources []string
ResourceNames []string
Verbs []string
NonResourceURLs []string
}
PolicyRule is one grant inside a Role/ClusterRole: a set of verbs allowed on a set of resources (or non-resource URLs). Kubernetes evaluates rules additively — a subject may do X if *any* bound rule permits it — so risk is a union, not an intersection, and our analysis treats it that way.
type Report ¶
Report is the full result of an analysis: the ordered risks plus the resolved graph (so callers can run additional who-can queries). Counts are precomputed for quick summaries.
func Analyze ¶
Analyze runs every enabled check over a parsed cluster and returns an ordered Report. It is the single analysis entry point used by the module and the CLI.
func AnalyzeBytes ¶
AnalyzeBytes is a convenience wrapper: parse JSON then analyze.
func AnalyzePath ¶
AnalyzePath is a convenience wrapper: parse a file/dir then analyze.
func (*Report) Highest ¶
Highest returns the most severe risk level present, or SeverityUnknown when the report is clean. Frontends use it for exit codes and gating.
type Risk ¶
type Risk struct {
RuleID string
Severity engine.Severity
Title string
Description string
// Subject is the principal at fault (subject key) when applicable; Resource
// is the object at fault (role/binding name) otherwise. At least one is set.
Subject string
Resource string
Remediation string
References []string
// Path, when set, is a human-readable escalation chain (pod → … →
// cluster-admin) that justifies an escalation finding.
Path []string
// Meta carries structured, machine-consumable context for agent remediation
// (the "explain & auto-remediate" mandate): the exact verb/resource that
// triggered the rule, the namespace, and so on.
Meta map[string]string
}
Risk is one identity/RBAC problem found in the cluster. It is engine-agnostic (no import of the Finding type in the pure model beyond severity), so the analysis library can be tested and reused without the module layer. The rbac engine module maps Risk directly onto engine.Finding.
type Role ¶
type Role struct {
Name string
Namespace string // empty when ClusterScoped
ClusterScoped bool
Rules []PolicyRule
Labels map[string]string
// Aggregates is true when the role uses aggregationRule to absorb other
// roles' rules; such roles are living permission sets and worth flagging.
Aggregates bool
}
Role is a namespaced or cluster-scoped set of permissions. We fold ClusterRole into the same type and distinguish with ClusterScoped, because their rule semantics are identical — only their reach differs.
func GenerateLeastPrivilege ¶
func GenerateLeastPrivilege(subjectKey string, observed []Permission) *Role
GenerateLeastPrivilege builds the minimal Role that would cover exactly the permissions a subject was observed using (from audit data). It is the constructive counterpart to the risk report: instead of only saying "this is over-broad", it hands back the tight role to replace it with. Permissions are grouped by apiGroup and rendered deterministically.
type ServiceAccount ¶
type ServiceAccount struct {
Name string
Namespace string
AutomountToken *bool
Labels map[string]string
// LastUsed, when known (e.g. from an audit-usage fixture), lets the NHI
// pass flag dormant automation identities. Zero means unknown.
LastUsedUnix int64
// Cloud, when non-nil, is the cloud IAM identity this ServiceAccount is
// federated to via workload identity (IRSA / GKE / AKS). It is the bridge
// where a compromised pod's K8s token becomes cloud credentials.
Cloud *CloudIdentity
}
ServiceAccount is a machine identity. AutomountToken mirrors the Kubernetes field: nil means "unset" (which defaults to mounting a token), so nil is not the same as false and we treat it as automounting.
type SocketMount ¶
SocketMount records a container that has the Docker daemon socket bind-mounted in — full control of the host's containers from inside one of them.