RBAC Basics - Role-Based Access Control and Least Privilege
Status: Active
Last Updated: 2026-08-14
Category: Security - Phase 4: Secret & Access Management
Prerequisites: user-account-security, vault-introduction
Time: 2-3 hours
Tags: rbac, policies, least-privilege, audit, vault, authorization
Summary
"Everyone is admin" works until the day it doesn't. Role-Based Access Control replaces blanket trust with explicit answers to two questions: which identity, may do what, to which resource? This lesson teaches RBAC as a transferable pattern โ implemented concretely in HashiCorp Vault (HCL policies), Linux sudoers, and Kubernetes RBAC โ plus audit logging so you can prove what access actually happened, not just what should have.
๐ฏ What You'll Learn
- โ Explain subjects โ roles/permissions โ resources and how mapping works
- โ Apply the principle of least privilege without making operations impossible
- โ Write Vault ACL policies: capabilities, globs, deny precedence
- โ Mirror the pattern in sudoers and Kubernetes RoleBindings
- โ Enable Vault audit logging and read an audit record
- โ Run periodic access reviews that catch permission drift
The RBAC Model
Three primitives, everywhere:
SUBJECTS ROLES / POLICIES RESOURCES
(humans, apps, (named bundles of (paths, files,
service accts) permissions) API routes)
jane โโโโโโโ
ci-bot โโโโโผโโโถ [deployer] = read+write apps/* โโโถ secret/apps/*
intern โโโโโ [auditor] = read-only sys/audit โโโถ sys/*
Key design decisions:
- Assign people/applications to roles, never permissions directly. When Jane joins the platform team you add her to
platform-team, not 40 individual grants. - Roles map to job function, not to individuals. People churn; functions persist.
- Default deny. Anything not explicitly granted is refused. Every system here works this way if you configure it honestly.
Least Privilege in Practice
The failure mode of least privilege isn't "too strict" โ it's permission creep: temporary grants ("just for the migration") that never die. Countermeasures:
- Time-bound every grant at creation (
ttl,duration:fields). - Review quarterly: every permission must have a named owner who still defends it.
- Prefer capability-scoped roles (
can-read-config) over god roles (admin).
Implementation 1: Vault Policies (HCL)
Vault policies attach capabilities to path prefixes:
platform-team.hcl:
# Read all shared app secrets
path "secret/data/apps/*" {
capabilities = ["read", "list"]
}
# Full control over team-owned paths
path "secret/metadata/apps/platform/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# Everyone may look up their own token properties
path "auth/token/lookup-self" {
capabilities = ["read"]
}
Capabilities vocabulary:
| Capability | Meaning |
|---|---|
read |
GET data |
create |
PUT when path doesn't exist yet |
update |
PUT on existing path |
delete |
DELETE |
list |
enumerate children (metadata, not values) |
sudo |
access "root-protected" endpoints (e.g., unseal) |
deny |
hard block โ wins over any other grant |
Install and use:
export VAULT_ADDR=http://127.0.0.1:8200 VAULT_TOKEN=hvs.root
vault policy write platform-team platform-team.hcl
vault policy write auditor auditor.hcl
vault token create -policy=platform-team -policy=auditor -ttl=8h
What Happens: glob rules are prefix matches evaluated most-specific-wins, with deny always trumping. Test like an attacker before trusting it:
TOKEN=hvs.new-token
curl -s -H "X-Vault-Token: $TOKEN" \
$VAULT_ADDR/v1/secret/data/infra/tf-state # expect: permission denied โ
curl -s -H "X-Vault-Token: $TOKEN" \
$VAULT_ADDR/v1/secret/data/apps/website/config # expect: data โ
Implementation 2: Linux sudoers (Same Pattern)
# /etc/sudoers.d/deploy-team (always edit via visudo -f)
%deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart website, \
/usr/bin/journalctl -u website, \
/usr/local/bin/deploy.sh
What Happens: the deploy group can restart and inspect exactly one service and run one script โ not ALL. This is RBAC where "resources" are binaries and "roles" are groups. Never hand out blanket (ALL) NOPASSWD: ALL; see user-account-security.
Implementation 3: Kubernetes RBAC
apiVersion: rbac.authorization.k8s.io/v1
kind: Role # namespace-scoped
metadata:
name: website-deployer
namespace: prod
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "update"] # no delete, no create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ci-to-website-deployer
namespace: prod
subjects:
- kind: ServiceAccount
name: ci-runner
namespace: cicd
roleRef:
kind: Role
name: website-deployer
apiGroup: rbac.authorization.k8s.io
Same shape: subject (ci-runner SA), role (website-deployer), verbs scoped tighter than "everything on deployments". Verify empirically:
kubectl auth can-i update deployments -n prod --as=system:serviceaccount:cicd:ci-runner # yes
kubectl auth can-i delete deployments -n prod --as=system:serviceaccount:cicd:ci-runner # no โ
Audit Logging (Prove What Happened)
Policies say what should happen; audit logs record what did. Enable Vault's tamper-evident log first thing after init:
mkdir -p /var/log/vault && chown 100:100 /var/log/vault # match container user
docker exec -e VAULT_ADDR=$VAULT_ADDR -e VAULT_TOKEN=$VAULT_TOKEN vault \
vault audit enable file file_path=/vault/log/audit.log
(add -v ./log:/vault/log to your compose volume list). Every request now produces a hashed-chain JSON entry โ secrets appear as HMAC'd ciphertext in logs, safe to ship to your aggregator (../observability/why-monitor).
Reading one record:
{
"time": "2026-08-14T09:12:33Z",
"type": "response",
"auth": { "display_name": "ldap-jane", "policies": ["platform-team"], ... },
"request": { "operation": "read", "path": "secret/data/apps/website/config" },
"error": ""
}
Who (display_name, policies), did what (operation, path), when, success or error. Alert-worthy events: any denied from unexpected principals, reads of personal/* outside business hours, use of sudo-capability paths.
For Linux hosts, journalctl _COMM=sudo and lastb give equivalent trails; forward both into the same store.
Quarterly Access Review Checklist
Export current grants:
vault policy list && vault auth list kubectl get rolebindings,clusterrolebindings -A getent group deploy sudo dockerFor each grant: named owner? Still employed/still deployed? Used in last 90 days? (Vault audit log + K8s audit policy answer usage; grep journals for sudo.)
Delete anything failing those tests. Silence is consent for drift.
Document decisions in the ticket โ Ticketing is Truth.
Troubleshooting & Common Issues
| Symptom | Cause | Fix |
|---|---|---|
permission denied though policy looks right |
Token predates policy update | Re-issue token; policies bind at issuance (except with token_policies updates + renewal) |
| Can read but not list (or vice versa) | read โ list in Vault semantics |
Grant each explicitly; list returns names only |
deny seems ignored |
More-specific rule overriding? No โ check typo/path mismatch | vault policy read <name>; remember deny always wins when matched |
| sudoers edit breaks sudo entirely | Syntax error | Only ever edit with visudo -f; keep a root shell open while testing |
| K8s binding exists but still denied | Wrong namespace (Role vs ClusterRole scope) or SA namespace | Check roleRef.kind and subject namespace |
| Nobody knows why access was revoked | Audit log not enabled / rotated away | Ship audit logs off-host with retention โฅ 1 year |
๐ Related
- Previous: identity-management โ where subjects come from
- Next: zero-trust-principles โ RBAC as one pillar of ZT
- vault-authentication โ attaching these policies via auth methods
- vault-secrets โ path layout being protected here
- container-security โ same least-privilege thinking applied to containers
- compliance-automation โ proving RBAC hygiene to auditors
Change Log
- 2026-08-14 โ Initial lesson created as part of KB course build-out (security Phase 4).
Next Steps / Ops Actions
- Verify Kubernetes bindings reference correct Role/ClusterRole kinds and namespaces for each service account.
- Enable audit logging (off-host, โฅ1 year retention) before granting any new roles in production.
- Run
visudo -fsyntax checks before committingsudoerschanges; keep a root shell open during edits. - Document least-privilege role mappings in
sysadmin/system-admin-basics.mdwith rotation schedules.