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


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:

  1. Assign people/applications to roles, never permissions directly. When Jane joins the platform team you add her to platform-team, not 40 individual grants.
  2. Roles map to job function, not to individuals. People churn; functions persist.
  3. 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:


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

  1. Export current grants:

    vault policy list && vault auth list
    kubectl get rolebindings,clusterrolebindings -A
    getent group deploy sudo docker
    
  2. For 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.)

  3. Delete anything failing those tests. Silence is consent for drift.

  4. 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


Change Log

Next Steps / Ops Actions

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse