Challenge 1: Password protect the SCADA¶
What you can do: Add username/password authentication to the OPC UA SCADA servers so that anonymous connections are rejected.
Difficulty: Beginner
Time: 20-30 minutes
The problem¶
Right now, anyone who can reach UU Power and Light’s network can connect to the SCADA servers and read or write process values. No username, no password, no questions asked. This is, unfortunately, how many real industrial systems are deployed: “it worked during commissioning” becomes the permanent security policy.
An attacker on the network could change turbine setpoints, mask alarms, or push the reactor into dangerous operating conditions, all without identifying themselves.
Your job is to fix that.
Before starting¶
Make sure the simulator is running:
python tools/simulator_manager.py
You will need a second terminal for the commands below. Activate the virtual environment in that terminal too.
Part 1: See the problem¶
Before you fix anything, it helps to see exactly how bad things are.
Check the current security status¶
python tools/blue_team.py opcua status
Look for the line that says Authentication: DISABLED (VULNERABLE). That tells you every OPC UA server is accepting anonymous connections.
See which users exist¶
python tools/blue_team.py opcua list-users
This shows the five users already configured in the system (operator1, engineer1, supervisor1, admin, viewer1). They have roles and permissions, but none of that matters yet because nobody is required to log in.
Try connecting anonymously¶
There is already an attack script for this. Run it:
python scripts/vulns/opcua_readonly_probe.py
This connects to the OPC UA server without any credentials, browses the object hierarchy, reads server status, and saves a report. If it succeeds (and it will), you have just demonstrated the vulnerability: anyone on the network can do this.
Have a look at the script itself (scripts/vulns/opcua_readonly_probe.py) to see what it does. It is short and worth reading.
If you want to go further and try writing a value anonymously, you can do that too:
import asyncio
from asyncua import Client
async def anonymous_write():
"""Write to SCADA without any credentials."""
async with Client("opc.tcp://localhost:4840/") as client:
temp_node = client.get_node("ns=2;s=Temperature")
temp = await temp_node.read_value()
print(f"Current temperature: {temp}")
await temp_node.write_value(999.9)
print("Wrote Temperature=999.9. No credentials needed.")
asyncio.run(anonymous_write())
That is the real problem. Not just reading, but writing. An attacker could change setpoints, mask alarms, or cause dangerous conditions.
Part 2: Enable authentication¶
Now close the door.
Step 1: Edit the configuration¶
Open config/opcua_security.yml and change:
require_authentication: true
That single line tells the OPC UA servers to check usernames against the RBAC user database. Anonymous connections will be refused.
Step 2: Restart the simulator¶
Authentication settings take effect at startup, not while running. This is realistic: you cannot swap authentication on a live OPC UA server without restarting it.
python tools/simulator_manager.py
Watch the startup output. You should see something like:
OPC UA authentication enforcement: scada_server_primary (users: 5)
Step 3: Confirm it worked¶
In your second terminal:
python tools/blue_team.py opcua status
You should now see Authentication: ENABLED and a list of the active users with their OPC UA roles.
Part 3: Test defences¶
Anonymous access should fail¶
Run the same probe script from Part 1:
python scripts/vulns/opcua_readonly_probe.py
This time it should fail with a connection error. If it still connects, something went wrong with the configuration or restart.
You can also test explicitly in Python:
import asyncio
from asyncua import Client
async def anonymous_attempt():
"""This should be rejected now."""
try:
async with Client("opc.tcp://localhost:4840/") as client:
temp = client.get_node("ns=2;s=Temperature")
value = await temp.read_value()
print(f"Still works! Something is wrong. Value: {value}")
except Exception as e:
print(f"Good: anonymous access rejected. ({e})")
asyncio.run(anonymous_attempt())
Authenticated access should succeed¶
import asyncio
from asyncua import Client
async def authenticated_access():
"""Connect with valid credentials."""
client = Client("opc.tcp://localhost:4840/")
client.set_user("operator1")
client.set_password("operator123")
async with client:
temp = client.get_node("ns=2;s=Temperature")
value = await temp.read_value()
print(f"Authenticated successfully. Temperature: {value}")
asyncio.run(authenticated_access())
An unknown user should be rejected¶
import asyncio
from asyncua import Client
async def unknown_user():
"""Try connecting with credentials that are not in the database."""
client = Client("opc.tcp://localhost:4840/")
client.set_user("hacker")
client.set_password("password123")
try:
async with client:
print("This should not have worked.")
except Exception as e:
print(f"Good: unknown user rejected. ({e})")
asyncio.run(unknown_user())
Part 4: Think bbout it¶
You have just deployed the first layer of defence. Take a moment to consider what it does and does not protect you from.
What authentication gives you¶
Anonymous attackers are locked out
Every connection is tied to a username
You can audit who connected and when
What it does not give you¶
Authentication only controls who can connect. It says nothing about what they can do once connected, and it does not protect the credentials in transit.
| Layer | Challenge | What it controls | Without it | |-|–||-| | Authentication | 1 (this) | Who can connect | Anyone connects anonymously | | Authorisation | 2 (RBAC) | What they can do | Authenticated users can do anything | | Encryption | 7 | Data in transit | Passwords sent in cleartext |
Think about this sequence from an attacker’s perspective:
No authentication: Connect anonymously, read and write everything
Authentication only: Need a username, but the password travels in cleartext (sniff the network, grab credentials)
Authentication + encryption: Credentials are protected, but once logged in, full access to every operation
Authentication + encryption + RBAC: Credentials protected, and each user can only do what their role allows
Each layer closes a gap that the previous one left open.
Trade-offs worth discussing¶
Operators who are used to anonymous access now need to remember credentials. What happens when someone forgets their password at 3am during an incident?
This simulation accepts any password for a known username. Real systems would need proper password management or integration with Active Directory/LDAP.
Username/password authentication is a big improvement over anonymous access, but certificate-based authentication (Challenge 7) is stronger in production.
Bonus challenges¶
If you have time, try these:
Lock a user account and verify they can no longer connect:
python tools/blue_team.py rbac lock-user --username operator1 --reason "Testing account lockout"Then try connecting as operator1. What happens?
Check the audit log for authentication events:
python tools/blue_team.py audit query --category securityCombine with encryption (Challenge 7): Enable both
require_authentication: trueandenforcement_enabled: trueinconfig/opcua_security.ymlfor full protection.Run the demo script to see authentication working without starting a full server:
python examples/opcua_auth_demo.py
Quick reference¶
Configuration file: config/opcua_security.yml
require_authentication: false # Change to true
Useful commands:
python tools/blue_team.py opcua status # Check authentication status
python tools/blue_team.py opcua list-users # See users and role mappings
python tools/blue_team.py status # Overall security overview
Requires restart: Yes. OPC UA security settings are bound to the server endpoint and cannot be changed on a live connection.