rbac

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 9 Imported by: 0

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.

func LoadBytes

func LoadBytes(data []byte) (*Cluster, error)

LoadBytes parses a single JSON document (an object or a Kubernetes List) into a Cluster. Unknown kinds are ignored, not errored: real exports mix in objects we do not model, and dropping them is the correct, quiet behavior.

func LoadPath

func LoadPath(path string) (*Cluster, error)

LoadPath loads from a file or, if path is a directory, every *.json file under it (bounded). It is the entry point the `dsecrat rbac <path>` command uses.

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

func (g *Graph) WhoCan(verb, apiGroup, resource, namespace string) []Subject

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

type Report struct {
	Risks  []Risk
	Graph  *Graph
	Counts map[engine.Severity]int
}

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

func Analyze(c *Cluster, opts Options) *Report

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

func AnalyzeBytes(data []byte, opts Options) (*Report, error)

AnalyzeBytes is a convenience wrapper: parse JSON then analyze.

func AnalyzePath

func AnalyzePath(path string, opts Options) (*Report, error)

AnalyzePath is a convenience wrapper: parse a file/dir then analyze.

func (*Report) Highest

func (r *Report) Highest() engine.Severity

Highest returns the most severe risk level present, or SeverityUnknown when the report is clean. Frontends use it for exit codes and gating.

func (*Report) Text

func (r *Report) Text() string

Text renders the report as a stable, human-readable summary. It is deterministic (risks are already sorted) and safe to snapshot in a golden test.

func (*Report) WhoCan

func (r *Report) WhoCan(verb, apiGroup, resource, namespace string) []Subject

WhoCan is a passthrough to the graph's reverse query, letting CLI/agent callers ask "who can <verb> <resource> in <namespace>" against an analyzed report.

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 RoleRef

type RoleRef struct {
	Kind string // "Role" | "ClusterRole"
	Name string
}

RoleRef points a binding at the Role or ClusterRole it grants.

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

type SocketMount struct {
	Container string
	Path      string
}

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.

type Subject

type Subject struct {
	Kind      string // "User" | "Group" | "ServiceAccount"
	Name      string
	Namespace string
}

Subject is a principal a binding grants a role to: a User, Group, or ServiceAccount. For ServiceAccounts, Namespace is meaningful.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL