Challenge 2: Role-based access control (RBAC)

Objective: Implement role-based access control to ensure operators, engineers, and supervisors can only perform actions appropriate to their privilege level.

Category: Authentication & Authorisation

Difficulty: Beginner-Intermediate

Time Required: 30-45 minutes

Learning outcomes

By completing this challenge, you will:

  1. Understand ICS role-based access control (RBAC) principles

  2. Configure RBAC enforcement for device write operations

  3. Test permission boundaries with different user roles

  4. Respond to privilege escalation attempts

  5. Understand config (persistent) vs runtime (temporary) changes

  6. Audit access denials and security violations

Background: ICS RBAC principles

Industrial control systems require separation of duties to prevent:

  • Operators accidentally reconfiguring safety parameters

  • Unauthorized personnel tampering with process setpoints

  • Single-point compromise leading to full system control

ICS User roles:

Role

Privilege Level

Typical Permissions

VIEWER

Read-only

View data, view alarms

OPERATOR

Control operations

View data + control setpoints, start/stop equipment

ENGINEER

Configuration

Operator permissions + program PLCs, configure parameters

SUPERVISOR

Elevated control

Engineer permissions + bypass safety, force I/O

ADMIN

Full system access

All permissions + user management

Key Principle: Least privilege. Users get minimal permissions needed for their job function.

Initial state (vulnerable)

Before hardening, the simulation has:

  • Expected resultUser accounts exist (operator1, engineer1, supervisor1, admin, viewer1)

  • Expected resultRoles and permissions defined in components/security/authentication.py

  • ❌ RBAC enforcement DISABLED: enforcement_enabled: false

  • ❌ Any user can write to any device memory address

  • ❌ No permission checks on control operations

Result: Attacker with viewer credentials can modify PLC setpoints and trigger safety bypasses.

Part 1: Configuration changes (require restart)

Configuration changes establish baseline RBAC enforcement that persists across restarts.

Step 1.1: Enable RBAC enforcement

Create/edit config/rbac.yml:

enforcement_enabled: true  # Enable RBAC permission checks
log_denials: true          # Log all permission denials

# Map memory address patterns to required permissions
address_permissions:
  # Read operations (any authenticated user)
  ".*": VIEW_DATA

  # Control operations (OPERATOR and above)
  "^(holding_registers|coils)\\[\\d+\\]$": CONTROL_SETPOINT
  "^ns=2;s=(Start|Stop|Enable|Disable).*": CONTROL_START_STOP
  "^ns=2;s=Mode": CONTROL_MODE_CHANGE

  # Configuration operations (ENGINEER and above)
  "^DB\\d+\\..*": CONFIG_PROGRAM          # S7 PLC data blocks
  "^Program:.*": CONFIG_PROGRAM           # AB Logix programs
  "retention_days": CONFIG_PARAMETER      # Historian config
  "alarm_setpoint": CONFIG_PARAMETER      # Alarm thresholds

  # Safety-critical operations (SUPERVISOR only)
  "^ns=2;s=Safety.*": SAFETY_BYPASS
  "^.*_force_.*": SAFETY_FORCE
  "scram": EMERGENCY_SHUTDOWN

What this does:

  • Enables permission checks in DataStore.write_memory()

  • Maps memory address patterns to required permissions

  • Blocks writes that exceed user’s permission level

  • Logs all permission denials to audit trail

Step 1.2: Configure session timeout

Edit config/simulation.yml:

authentication:
  session_timeout_hours: 8.0  # 8 simulation hours
  require_session: true        # Require valid session for writes

What this does:

  • Sessions expire after 8 simulation hours of inactivity

  • Expired sessions cannot perform writes (session validation required)

Step 1.3: Restart simulation

# Stop current simulation (Ctrl+C)
# Restart with new config
python tools/simulator_manager.py

Part 2: Verify baseline RBAC

Test that RBAC enforcement is active using the Blue Team CLI.

Step 2.1: Test viewer permissions (read-only)

Terminal 1 (Simulation running):

# View simulation state - should succeed
python tools/blue_team.py rbac list-users

Terminal 2 (Attack script):

# Attempt to write as viewer (should FAIL)
python scripts/vulns/modbus_write.py \
  --target hex_turbine_plc \
  --address 0 \
  --value 9999 \
  --username viewer1

Expected result:

❌ Permission denied: User 'viewer1' (VIEWER) lacks permission CONTROL_SETPOINT

Check audit log:

python tools/blue_team.py rbac audit-log --user viewer1

You should see:

[2024-03-15 10:30:45] DENIED: viewer1 (VIEWER) - CONTROL_SETPOINT on turbine_plc:holding_registers[0]

Step 2.2: Test operator permissions (control allowed)

# Operator can write setpoints (should SUCCEED)
python scripts/vulns/modbus_write.py \
  --target hex_turbine_plc \
  --address 0 \
  --value 3600 \
  --username operator1

Expected result:

✓ Write successful: holding_registers[0] = 3600

Step 2.3: Test operator boundary (cannot program)

# Operator cannot program PLCs (should FAIL)
python scripts/vulns/s7_db_write.py \
  --target reactor_plc \
  --db 1 \
  --variable setpoint \
  --value 350 \
  --username operator1

Expected: Permission denied: User 'operator1' (OPERATOR) lacks permission CONFIG_PROGRAM

Step 2.4: Test Engineer Permissions (Programming Allowed)

# Engineer can program PLCs (should SUCCEED)
python scripts/vulns/s7_db_write.py \
  --target reactor_plc \
  --db 1 \
  --variable setpoint \
  --value 350 \
  --username engineer1

Expected: Program successful: DB1.setpoint = 350

Part 3: Runtime incident response

Runtime changes provide immediate response to active incidents but are lost on restart.

Scenario 1: Compromised operator account

Situation: IDS detects that operator1 is attempting to program PLCs (privilege escalation).

Response: Temporarily downgrade operator to viewer role.

# Downgrade compromised account (immediate effect)
python tools/blue_team.py rbac change-role \
  --username operator1 \
  --role VIEWER \
  --user security_admin \
  --reason "Account compromise detected - privilege escalation attempt"

Verify:

# Operator1 can no longer write setpoints
python scripts/vulns/modbus_write.py \
  --target hex_turbine_plc \
  --address 0 \
  --value 9999 \
  --username operator1

Expected: Permission denied: User 'operator1' (VIEWER) lacks permission CONTROL_SETPOINT

Scenario 2: Emergency operator elevation

Situation: All engineers are offsite. Experienced operator needs temporary engineer access.

Response: Temporarily elevate operator to engineer role.

# Elevate operator (immediate effect)
python tools/blue_team.py rbac change-role \
  --username operator1 \
  --role ENGINEER \
  --user supervisor1 \
  --reason "Emergency maintenance - all engineers offsite"

Verify:

# Operator1 can now program PLCs
python scripts/vulns/s7_db_write.py \
  --target reactor_plc \
  --db 1 \
  --variable setpoint \
  --value 320 \
  --username operator1

Expected: Program successful: DB1.setpoint = 320

Important: Runtime role changes are temporary and lost on restart. To make permanent, edit user database or config.

Scenario 3: Disable RBAC (emergency override)

Situation: RBAC is blocking legitimate emergency operations during crisis.

Response: Temporarily disable RBAC enforcement.

# Disable RBAC enforcement (immediate effect)
python tools/blue_team.py rbac disable \
  --user admin \
  --reason "Emergency override - reactor SCRAM in progress"

Warning: This removes ALL permission checks. Use only in genuine emergencies.

Re-enable after emergency:

python tools/blue_team.py rbac enable \
  --user admin \
  --reason "Emergency resolved - restoring security controls"

Part 4: Permission mapping testing

Test different memory address patterns against roles:

Test matrix

Address Pattern

Example

Minimum Role

Notes

holding_registers[N]

holding_registers[0]

OPERATOR

Modbus setpoints

coils[N]

coils[5]

OPERATOR

Modbus digital outputs

ns=2;s=Start*

ns=2;s=StartPump

OPERATOR

OPC UA control

DB*.variable

DB1.setpoint

ENGINEER

S7 PLC programming

Program:*

Program:MainLoop

ENGINEER

Logix programming

retention_days

retention_days

ENGINEER

Historian config

ns=2;s=Safety*

ns=2;s=SafetyBypass

SUPERVISOR

Safety bypass

*_force_*

pump_force_on

SUPERVISOR

Force I/O

scram

scram

SUPERVISOR

Emergency shutdown

Test script

Run automated permission tests:

# Test all permission boundaries
python tests/security/test_rbac_boundaries.py

Expected output:

Testing VIEWER permissions...
  ✓ READ holding_registers[0]: ALLOWED
  ✓ WRITE holding_registers[0]: DENIED
  ✓ WRITE DB1.setpoint: DENIED
  ✓ WRITE scram: DENIED

Testing OPERATOR permissions...
  ✓ READ holding_registers[0]: ALLOWED
  ✓ WRITE holding_registers[0]: ALLOWED
  ✓ WRITE DB1.setpoint: DENIED
  ✓ WRITE scram: DENIED

Testing ENGINEER permissions...
  ✓ READ holding_registers[0]: ALLOWED
  ✓ WRITE holding_registers[0]: ALLOWED
  ✓ WRITE DB1.setpoint: ALLOWED
  ✓ WRITE scram: DENIED

Testing SUPERVISOR permissions...
  ✓ READ holding_registers[0]: ALLOWED
  ✓ WRITE holding_registers[0]: ALLOWED
  ✓ WRITE DB1.setpoint: ALLOWED
  ✓ WRITE scram: ALLOWED

Part 5: Making runtime changes permanent

Runtime changes (Blue Team CLI) are temporary and lost on restart.

Edit config/rbac.yml to add permanent role changes:

enforcement_enabled: true

# Override default roles for specific users
user_role_overrides:
  operator1: ENGINEER  # Permanently promoted

Then restart:

python tools/simulator_manager.py

Part 6: Advanced Scenarios

Scenario 1: Insider Threat (Privilege Escalation)

Attacker: Compromised operator account attempts to escalate privileges.

Detection:

# Monitor for permission denials
python tools/blue_team.py rbac audit-log --filter denied

Response:

  1. Identify user with repeated denials

  2. Check what permissions they’re attempting

  3. Downgrade or lock account:

python tools/blue_team.py rbac lock-user \
  --username operator1 \
  --user security_admin \
  --reason "Multiple privilege escalation attempts"

Scenario 2: Role Confusion (Wrong Role Assignment)

Problem: New engineer accidentally given operator role, cannot perform duties.

Detection: User reports inability to program PLCs.

Response:

# Check user's current role
python tools/blue_team.py rbac list-users --username engineer2

# Fix role assignment
python tools/blue_team.py rbac change-role \
  --username engineer2 \
  --role ENGINEER \
  --user admin \
  --reason "Correcting role assignment error"

Scenario 3: Dual Authorisation (Safety-Critical)

Requirement: Safety bypass operations require TWO supervisors to approve.

Implementation:

# In device code
result = await auth_mgr.authorize_with_dual_auth(
    session_id_1=supervisor1_session,
    session_id_2=supervisor2_session,
    action=PermissionType.SAFETY_BYPASS,
    resource="reactor_safety_plc:bypass_interlock_1"
)

Test:

# Attempt single-user safety bypass (should FAIL)
python scripts/vulns/safety_bypass.py \
  --target reactor_safety_plc \
  --function bypass_interlock \
  --username supervisor1

# Attempt dual-user safety bypass (should SUCCEED)
python scripts/vulns/safety_bypass_dual.py \
  --target reactor_safety_plc \
  --function bypass_interlock \
  --user1 supervisor1 \
  --user2 supervisor2

Part 7: Config vs Runtime Changes

Aspect

Config Changes (YAML)

Runtime Changes (CLI)

Takes effect

After restart

Immediately

Persistence

Permanent (stored in files)

Temporary (lost on restart)

Use case

Baseline security policy

Incident response

Examples

Enable RBAC, permission mappings

Change user role, lock account

Audit trail

Git history, file timestamps

SystemState audit log

Approval

Change control process

Security team emergency

Best Practice:

  1. Config: Set secure baseline (RBAC enabled, proper mappings)

  2. Runtime: Respond to incidents (downgrade compromised accounts)

  3. Follow-up: Update config to reflect permanent changes

Testing procedures

Pre-hardening test (should SUCCEED)

# RBAC disabled - viewer can write (VULNERABLE)
python scripts/vulns/modbus_write.py \
  --target hex_turbine_plc \
  --address 0 \
  --value 9999 \
  --username viewer1

Expected: ✓ Write successful

Post-hardening test (should FAIL)

# RBAC enabled - viewer cannot write (PROTECTED)
python scripts/vulns/modbus_write.py \
  --target hex_turbine_plc \
  --address 0 \
  --value 9999 \
  --username viewer1

Expected: ❌ Permission denied

Verification checklist

  • Viewer can read but cannot write

  • Operator can control setpoints but cannot program

  • Engineer can program PLCs but cannot bypass safety

  • Supervisor can bypass safety systems

  • Permission denials appear in audit log

  • Runtime role changes take effect immediately

  • Runtime changes lost after restart

  • Config changes persist after restart

Common issues and solutions

Issue 1: “RBAC enforcement enabled but writes still succeed”

Cause: session_id not being passed to write_memory() calls.

Solution: Ensure attack scripts pass --username parameter, which creates session.

Issue 2: “All writes blocked including admin”

Cause: Admin role not in ROLE_PERMISSIONS mapping.

Solution: Check components/security/authentication.py - UserRole.ADMIN should have set(PermissionType) (all permissions).

Issue 3: “Runtime role change not taking effect”

Cause: Script using cached session from before role change.

Solution: Force new authentication:

# Logout old session
python tools/blue_team.py rbac logout --username operator1

# Try write again (will create new session with new role)
python scripts/vulns/modbus_write.py ...

Issue 4: “Config changes not loading”

Cause: Config file syntax error or wrong location.

Solution:

# Validate YAML syntax
python -c "import yaml; yaml.safe_load(open('config/rbac.yml'))"

# Check ConfigLoader output
python tools/simulator_manager.py --verbose

Assessment

Learning objectives

  • Can explain difference between user roles (Viewer/Operator/Engineer/Supervisor)

  • Can enable RBAC enforcement via config

  • Can test permission boundaries with different roles

  • Can respond to incidents using runtime role changes

  • Can make runtime changes permanent via config

  • Understands audit logging for permission denials

Practical skills

  • Successfully blocked viewer from writing setpoints

  • Successfully allowed operator to write setpoints

  • Successfully blocked operator from programming PLCs

  • Successfully allowed engineer to program PLCs

  • Successfully downgraded compromised account

  • Successfully located permission denials in audit log

Blue Team CLI Reference

RBAC Commands

# List all users and their roles
python tools/blue_team.py rbac list-users

# List specific user
python tools/blue_team.py rbac list-users --username operator1

# Change user role (runtime)
python tools/blue_team.py rbac change-role \
  --username USERNAME \
  --role ROLE \
  --user ADMIN_USER \
  --reason "REASON"

# Lock user account (runtime)
python tools/blue_team.py rbac lock-user \
  --username USERNAME \
  --user ADMIN_USER \
  --reason "REASON"

# Unlock user account (runtime)
python tools/blue_team.py rbac unlock-user \
  --username USERNAME \
  --user ADMIN_USER \
  --reason "REASON"

# Enable RBAC enforcement (runtime)
python tools/blue_team.py rbac enable \
  --user ADMIN_USER \
  --reason "REASON"

# Disable RBAC enforcement (runtime, DANGEROUS)
python tools/blue_team.py rbac disable \
  --user ADMIN_USER \
  --reason "REASON"

# View audit log
python tools/blue_team.py rbac audit-log

# View audit log for specific user
python tools/blue_team.py rbac audit-log --user operator1

# View only permission denials
python tools/blue_team.py rbac audit-log --filter denied

User roles

  • VIEWER - Read-only access

  • OPERATOR - Control operations (setpoints, start/stop)

  • ENGINEER - Configuration and programming

  • SUPERVISOR - Safety bypass and elevated control

  • ADMIN - Full system access

Additional resources

  • IEC 62443-4-2: Security for industrial automation and control systems

  • NIST SP 800-82: Guide to Industrial Control Systems (ICS) Security

  • ISA/IEC 62443-2-1: Security program requirements for IACS asset owners

  • Attack scripts: scripts/vulns/modbus_write.py, scripts/vulns/s7_db_write.py

  • Blue Team CLI: tools/blue_team.py

  • Authentication implementation: components/security/authentication.py

  • Config loader: config/config_loader.py

Next steps

After completing this challenge:

  1. Challenge 3: Implement logging and monitoring for security events

  2. Challenge 5: Implement function code filtering to block dangerous Modbus commands

  3. Challenge 6: Implement dual authorisation workflow for critical operations

  4. Challenge 9: Implement network segmentation to isolate zones