ROS 2 Security Guide 2026 — SROS2
Secure your ROS 2 robot with SROS2: create keystores and per-node enclaves, write governance.xml domain policies and permissions.xml topic access rules, enable DDS encryption with ROS_SECURITY_ENABLE, and debug authentication failures.
When You Need SROS2
SROS2 is essential for production deployments: hospital robots (PHI on the network), industrial cobots (prevent unauthorized command injection), autonomous vehicles (safety-critical commands), and any ROS 2 system exposed to untrusted networks. Development robots on isolated LANs can use ROS_SECURITY_STRATEGY=Permissive to test security without blocking communication.
SROS2 — DDS Security Overview
SROS2 (Secure ROS 2) implements the Object Management Group DDS Security specification. It provides authentication, encryption, and access control on top of the DDS transport — without changing application code.
SROS2 Security Architecture
═══════════════════════════════════════════════════════
DDS Security Spec (OMG) — 5 security plugins:
1. Authentication — PKIX X.509 mutual authentication (who are you?)
2. Access Control — XML policy files (what are you allowed to do?)
3. Cryptography — AES-GCM-GMAC encryption + HMAC signing
4. Logging — Security events to a dedicated topic
5. Data Tagging — Classify sensitive messages (optional)
Files per node (called an "enclave"):
identity_ca.cert.pem — Certificate Authority that issued node certs
cert.pem — Node's own X.509 certificate
key.pem — Node's private key (AES-256 encrypted)
permissions_ca.cert.pem — CA that signed the permissions XML
governance.xml(.p7s) — DDS domain-level security settings
permissions.xml(.p7s) — Per-node topic allow/deny rules
Key environment variables:
ROS_SECURITY_KEYSTORE — path to the keystore root directory
ROS_SECURITY_ENABLE — "true" to enable (default: "false")
ROS_SECURITY_STRATEGY — "Enforce" | "Permissive" | "Keystore"
ROS_SECURITY_ENCLAVE_SEARCH_METHOD — "MATCH_PREFIX" | "EXACT"
Supported in: ROS 2 Foxy and later (best support in Humble+)
Requires DDS: FastDDS (eProsima) or ConnextDDS (RTI) — not CycloneDDS⚡ SROS2 adds ~5-15% CPU overhead from encryption and signing. For edge robots with constrained compute, use PERMISSIVE mode in development and ENFORCE only in production deployments where security is required.
Create Keystore and Enclaves
ros2 security commands create a PKI keystore and per-node enclaves. Each node gets its own certificate signed by the identity CA.
# Step 1: Install SROS2 tools
sudo apt install -y ros-humble-rmw-fastrtps-cpp
pip install lxml
# Step 2: Create the keystore (PKI hierarchy)
ros2 security create_keystore ~/my_robot_keystore
# Structure created:
# ~/my_robot_keystore/
# ├── public/
# │ ├── ca.cert.pem (Identity CA certificate)
# │ └── permissions_ca.cert.pem (Permissions CA certificate)
# ├── private/
# │ ├── ca.key.pem (Identity CA private key — guard this!)
# │ └── permissions_ca.key.pem (Permissions CA private key)
# └── enclaves/ (one subfolder per ROS node)
# Step 3: Create an enclave for each node
# Enclave name = the ROS node's fully-qualified name
ros2 security create_enclave ~/my_robot_keystore /my_robot/lidar_node
ros2 security create_enclave ~/my_robot_keystore /my_robot/navigation
ros2 security create_enclave ~/my_robot_keystore /my_robot/controller
# Step 4: Create and sign governance.xml
ros2 security create_permission ~/my_robot_keystore /my_robot/lidar_node ~/policies/lidar_node_policy.xml
# Step 5: Verify the keystore
ros2 security list_enclaves ~/my_robot_keystoregovernance.xml — Domain-Level Policy
governance.xml sets security defaults for the DDS domain. It controls whether discovery is encrypted, whether unauthenticated participants are allowed, and the default topic security.
<?xml version="1.0" encoding="UTF-8"?>
<!-- governance.xml — applied to the whole DDS domain -->
<dds xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="omg_shared_ca_governance.xsd">
<domain_access_rules>
<domain_rule>
<!-- Apply to all domain IDs (0-232) -->
<domains>
<id_range>
<min>0</min>
<max>232</max>
</id_range>
</domains>
<!-- Only authenticated DDS participants allowed -->
<allow_unauthenticated_participants>FALSE</allow_unauthenticated_participants>
<enable_join_access_control>TRUE</enable_join_access_control>
<!-- Encrypt discovery (prevents scanning the network for topics) -->
<discovery_protection_kind>ENCRYPT</discovery_protection_kind>
<!-- Encrypt liveliness heartbeats -->
<liveliness_protection_kind>ENCRYPT</liveliness_protection_kind>
<!-- Per-topic security defaults (can be overridden in permissions.xml) -->
<topic_access_rules>
<topic_rule>
<topic_expression>*</topic_expression>
<!-- Require auth for pub/sub -->
<enable_discovery_protection>TRUE</enable_discovery_protection>
<enable_liveliness_protection>TRUE</enable_liveliness_protection>
<!-- Encrypt all messages by default -->
<metadata_protection_kind>ENCRYPT</metadata_protection_kind>
<data_protection_kind>ENCRYPT</data_protection_kind>
</topic_rule>
</topic_access_rules>
</domain_rule>
</domain_access_rules>
</dds>⚡ For performance-sensitive topics like /camera/image_raw, use data_protection_kind=SIGN instead of ENCRYPT. Signing adds authentication without the overhead of full encryption — images are large and encryption cost adds up.
permissions.xml — Per-Node Access Control
permissions.xml specifies which topics each node is allowed to publish to and subscribe from. The file is signed by the permissions CA private key.
<?xml version="1.0" encoding="UTF-8"?>
<!-- permissions.xml — access rules for /my_robot/lidar_node -->
<dds xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="omg_shared_ca_permissions.xsd">
<permissions>
<grant name="lidar_node_permissions">
<!-- Matches the CN= in the node's certificate -->
<subject_name>CN=/my_robot/lidar_node,O=RobotCA</subject_name>
<validity>
<not_before>2026-06-21T00:00:00</not_before>
<not_after>2036-06-21T23:59:59</not_after>
</validity>
<!-- Allow publishing (what this node produces) -->
<allow_rule>
<domains><id>0</id></domains>
<publish>
<topics>
<topic>/scan</topic>
<topic>/scan/compressed</topic>
<topic>/diagnostics</topic>
</topics>
</publish>
</allow_rule>
<!-- Allow subscribing (what this node reads) -->
<allow_rule>
<domains><id>0</id></domains>
<subscribe>
<topics>
<topic>/parameter_events</topic>
<topic>/rosout</topic>
</topics>
</subscribe>
</allow_rule>
<!-- Deny everything else -->
<default>DENY</default>
</grant>
</permissions>
</dds>Launch Nodes with Security Enabled
Set three environment variables to activate SROS2. The node binary is unchanged — security is handled by the RMW layer.
# ── Terminal: set env vars then run the node ─────────────────
export ROS_SECURITY_KEYSTORE=~/my_robot_keystore
export ROS_SECURITY_ENABLE=true
export ROS_SECURITY_STRATEGY=Enforce # or: Permissive (dev mode)
# Run the node normally — SROS2 handles authentication transparently
ros2 run my_pkg my_node --ros-args --enclave /my_robot/lidar_node
# ── Launch file (Python) ──────────────────────────────────────
from launch import LaunchDescription
from launch_ros.actions import Node
import os
def generate_launch_description():
keystore = os.path.expanduser("~/my_robot_keystore")
security_env = {
"ROS_SECURITY_KEYSTORE": keystore,
"ROS_SECURITY_ENABLE": "true",
"ROS_SECURITY_STRATEGY": "Enforce",
}
return LaunchDescription([
Node(
package="my_pkg",
executable="my_node",
name="lidar_node",
namespace="my_robot",
additional_env=security_env,
arguments=["--ros-args", "--enclave", "/my_robot/lidar_node"],
),
])
# ── Test mode: Permissive (logs violations but doesn't block) ──
# Permissive = nodes without valid certs still communicate,
# but violations are logged. Use for development/migration.
export ROS_SECURITY_STRATEGY=Permissive⚡ The enclave path (--enclave /my_robot/lidar_node) must exactly match the directory created by ros2 security create_enclave. If ROS_SECURITY_STRATEGY=Enforce and the enclave is missing, the node refuses to start.
Verify and Debug Secure Communications
Check that security is active and diagnose failures using ros2 security commands and FastDDS security logging.
# List all enclaves in the keystore
ros2 security list_enclaves ~/my_robot_keystore
# Check if a running node has security active
# (Secure nodes appear with enclave info in verbose topic info)
ros2 node info /my_robot/lidar_node
# Enable FastDDS security logging (verbose)
export FASTRTPS_DEFAULT_PROFILES_FILE=~/fastdds_security_debug.xml
# fastdds_security_debug.xml:
# <profiles>
# <participant profile_name="default_participant">
# <rtps>
# <propertiesPolicy>
# <properties>
# <property><name>dds.sec.log.plugin</name><value>builtin.DDS_LogTopic</value></property>
# <property><name>dds.sec.log.builtin.DDS_LogTopic.logging_level</name><value>DEBUG</value></property>
# </properties>
# </propertiesPolicy>
# </rtps>
# </participant>
# </profiles>
# Subscribe to the security log topic
ros2 topic echo /ros_security_logging
# Common error causes:
# "AUTHENTICATION_FAILED" → certificate mismatch or expired cert
# "ACCESS_CONTROL_VIOLATION" → node tried to pub/sub unauthorized topic
# "Couldn't find enclave" → ROS_SECURITY_KEYSTORE path wrong or enclave missingQuick Reference
| Command / Concept | Details |
|---|---|
| Create keystore | ros2 security create_keystore ~/keystore |
| Create enclave | ros2 security create_enclave ~/keystore /robot/node_name |
| List enclaves | ros2 security list_enclaves ~/keystore |
| Enable security | export ROS_SECURITY_ENABLE=true ROS_SECURITY_KEYSTORE=~/ks ROS_SECURITY_STRATEGY=Enforce |
| Specify enclave | ros2 run pkg node --ros-args --enclave /robot/node_name |
| Dev/test mode | ROS_SECURITY_STRATEGY=Permissive (logs but doesn't block) |
| governance.xml | Domain-level policy: allow_unauthenticated=FALSE, protection_kind=ENCRYPT |
| permissions.xml | Per-node allow_rule for <publish> and <subscribe> topics |
| ENCRYPT vs SIGN | SIGN=auth only (faster), ENCRYPT=auth+confidentiality (for sensitive data) |
| Debug | ros2 topic echo /ros_security_logging + FASTRTPS_DEFAULT_PROFILES_FILE for verbose log |