SROS2: ROS 2 Security with DDS-Security, Enclaves and Access Control
A default ROS 2 system is an open bus. Any process that joins the same DDS domain, whether a debugging laptop, a compromised container or an attacker on the warehouse Wi-Fi, can discover every node, subscribe to every camera topic, and publish a cmd_vel message that drives a two-tonne AMR into a rack. The middleware does not ask who you are. This is not a bug; it is the documented default, and it is the first thing a security reviewer will flag when a robot leaves the lab.
SROS2 is the answer the ROS 2 project ships: a set of tools and conventions that turn the OMG DDS-Security specification into something a roboticist can actually operate. It adds authentication, per-topic access control and encryption to ROS 2 without changing application code. It also has sharp edges: a keystore layout you must understand, XML policies that are signed, environment variables that fail open if misspelled, and a fleet-provisioning problem that the tooling leaves to you.
This post gives you the mental model, the file formats, the commands, the debugging playbook and a threat model for robot fleets, plus an honest look at where Zenoh-based security fits.
What this covers: the DDS-Security plugin architecture, enclaves and the keystore, governance and permissions files, certificate management, performance overhead, CI and fleet provisioning, common failures, and the Zenoh alternative.
Context and Background
ROS 2 is built on a layered stack. Application code calls the client libraries (rclcpp, rclpy), which sit on rcl, which calls a middleware abstraction called RMW (ROS Middleware Interface). Each RMW implementation, such as rmw_fastrtps_cpp, rmw_cyclonedds_cpp or rmw_connextdds, wraps a DDS vendor library. Security in ROS 2 is therefore not a ROS feature in the strict sense. It is the DDS-Security standard, published by the Object Management Group (OMG), exposed through RMW and configured by SROS2 artifacts.
DDS-Security defines five Service Plugin Interfaces (SPIs): authentication, access control, cryptography, logging and data tagging. ROS 2 uses the first three as its core and can use logging; data tagging is not exposed through ROS tooling. The standard fixes what the plugins must do, and vendors ship built-in implementations of the first three based on X.509 certificates, S/MIME-signed XML documents and AES-GCM. The SROS2 project documentation states that it has been tested with eProsima Fast DDS, Eclipse Cyclone DDS and RTI Connext DDS, with Connext Secure requiring a separate licence. You can read the project overview in the SROS2 repository and the middleware background in the official ROS 2 documentation.
Why did this take years to become routine? Three reasons. First, the original ROS 1 had no security story at all, so the community had no habits to port. Second, DDS-Security is a large specification, and its XML policy language is expressive but unforgiving. Third, robotics teams tend to treat security as a late-stage deployment concern, and by then they discover that every node’s identity, topic list and parameter surface needs to be enumerated. The work is mostly inventory, not cryptography.
There is also a competing path. rmw_zenoh_cpp is an alternative RMW built on Eclipse Zenoh, developed by the ROS community and packaged for recent distributions (check your distribution’s REP 2000 tier table for its current support level). Its security model is different: Zenoh uses TLS or mutual TLS (mTLS) for transport authentication and an access-control rule engine for authorisation. We compare the two later; if you are evaluating the broader Zenoh story, see our Zenoh and ROS 2 bridge tutorial.
One framing is worth fixing now. Security for robot fleets is rarely about confidentiality alone. A robot’s topics carry LiDAR, camera streams and maps (privacy and IP concerns), but the higher-consequence risk is integrity and availability: an attacker who can inject a goal pose, change a parameter or flood discovery can physically damage equipment or people. DDS-Security addresses integrity through signed and authenticated messages and availability only partially, as we will see in the threat model. If you build navigation stacks, as in our Nav2 MPPI trajectory validator analysis, the validity of every goal and costmap update depends on the trustworthiness of the publisher.
The SROS2 Reference Architecture
The short answer: SROS2 maps each ROS 2 node, via a name called an enclave, to an X.509 identity certificate plus a signed permissions file, and wires those into three DDS-Security plugins. The plugins authenticate peers at discovery, authorise each topic, service and action on a per-enclave basis, and encrypt and sign the data path. The RMW loads everything from a keystore directory named by environment variables.

Figure 1: Where SROS2 sits. Application code is unchanged; rcl reads the environment variables, the RMW passes file paths to the DDS vendor, and the three core DDS-Security plugins act on every discovery and data packet.
The figure shows the control flow that matters. Your node never calls a security API. At context initialisation, rcl reads the security environment variables, resolves the enclave directory, and hands the file paths to the RMW. The RMW then configures the participant with the vendor’s security properties. From that point, every DDS participant creation, every endpoint match and every sample passes through plugin code.
The four plugin responsibilities
Authentication answers “who is this participant?”. Each participant presents an identity certificate; the peer validates it against a configured identity CA. In the built-in plugin (named DDS:Auth:PKI-DH in the specification), the two participants run a mutual handshake that proves possession of the private key and establishes a shared secret using Diffie-Hellman. A participant that cannot complete the handshake is simply not matched. Importantly, authentication happens per participant, not per topic.
Access control answers “what may this participant do?”. The built-in plugin (DDS:Access:Permissions) uses two XML documents. The governance document declares domain-wide rules, such as whether discovery is protected and which protection kind applies to topics. The permissions document grants a specific subject, identified by its certificate subject name, the right to publish or subscribe on named topics within validity dates. Both documents are signed with S/MIME using the permissions CA, which is why you see .p7s files in the keystore.
Cryptography answers “how is the data protected?”. The built-in plugin (DDS:Crypto:AES-GCM-GMAC) derives keys per participant and per endpoint, and protects samples with AES-GCM for encryption or GMAC for authentication only. The governance document selects between ENCRYPT, SIGN and NONE as the protection kind per topic rule.
Logging answers “what happened?”. The logging plugin can emit security events, such as failed handshakes or denied permissions, to a log file or to a distinguished DDS topic. In practice, vendor support and ROS-level configuration of this plugin are uneven, and many deployments rely on ordinary DDS vendor logs plus ROS logging instead. Treat logging as available in the standard but not a feature you can assume.
Why enclaves and not nodes
Early SROS2 designs tied security artifacts to individual nodes. That became unworkable because DDS security applies to the participant, and a single ROS 2 process creates one participant per context, which may host many nodes. The concept that emerged is the enclave: a group of one or more nodes in a process (or processes sharing a context) that share one set of security artifacts.
Practically, an enclave is a name such as /robot1/navigation, and the keystore holds a directory of that name under enclaves/. You select the enclave at launch with the --ros-args -e /robot1/navigation option or the equivalent launch-file argument, or override it with an environment variable. The granularity decision is yours and matters a lot: one enclave per process gives tight least-privilege and a precise audit trail; one enclave per robot gives easy management and loose privilege. Most mature deployments land in between, with an enclave per functional subsystem (perception, navigation, drivers, teleoperation).
This is the central design trade-off, and I will state my opinion plainly: start with one enclave per process type, not per robot. The extra management burden is small once provisioning is scripted, and the privilege separation lets you give the teleoperation bridge a permission file that cannot touch motor controllers.
The environment variables
The variables are read in rcl. Based on the rcl security source, the following behaviour holds. ROS_SECURITY_ENABLE turns security on only when its value is exactly true; any other value, including unset, leaves it off. ROS_SECURITY_STRATEGY selects Enforce when set to exactly Enforce; anything else is permissive. ROS_SECURITY_KEYSTORE names the keystore root, and rcl looks for <keystore>/enclaves/<enclave>. ROS_SECURITY_ENCLAVE_OVERRIDE, if set, replaces the enclave name used for that lookup.
Two consequences are worth underlining. In permissive mode, if no enclave directory is found, the node starts anyway, unsecured, and logs only a warning. If security is enabled but no enclave is found and strategy is Enforce, initialisation fails. Therefore a deployment with ROS_SECURITY_ENABLE=true and no ROS_SECURITY_STRATEGY=Enforce can silently run in the clear after a typo in a path. Always set both. (Verification note: I confirmed the behaviour above against the rcl source; older distributions used ROS_SECURITY_ROOT_DIRECTORY and ROS_SECURITY_NODE_DIRECTORY, which are legacy names you may still see in old tutorials.)
The Keystore, Governance and Permissions in Detail
A keystore is a directory with a fixed layout, created and populated by the ros2 security command family from the sros2 package. Understanding the layout is the difference between guessing at failures and reading them.

Figure 2: Keystore layout. Private CA keys live in private/ and should be offline; every enclave gets a certificate, a private key and signed permission artifacts that chain back to the CAs.
Creating a keystore and enclaves
The commands are short:
export ROS_SECURITY_KEYSTORE=~/sros2_keystore
ros2 security create_keystore $ROS_SECURITY_KEYSTORE
ros2 security create_enclave $ROS_SECURITY_KEYSTORE /robot1/navigation
ros2 security create_enclave $ROS_SECURITY_KEYSTORE /robot1/perception
ros2 security list_enclaves $ROS_SECURITY_KEYSTORE
create_keystore generates the certificate authorities and a default governance document. In the layout produced by SROS2 (verify against your distribution, since details have shifted across releases), public/ holds CA certificates and the signed governance (governance.p7s, derived from governance.xml), while private/ holds the CA private keys. The enclaves/ directory is initially empty. SROS2 uses elliptic-curve keys on the prime256v1 (NIST P-256) curve and SHA-256 signatures, which are the algorithms DDS-Security requires for the built-in plugins (RSA-2048 is also permitted by the specification).
create_enclave generates a key pair and certificate for the enclave, signed by the identity CA, and writes into enclaves/robot1/navigation/ the files the RMW needs: the identity certificate and private key, the identity CA and permissions CA certificates, the signed governance and a signed permissions file. The enclave argument is a path-like name, so the folder structure mirrors it. The certificate subject carries the enclave name in its common name, and the permissions file names that subject. A mismatch between the two is a classic failure we revisit later.
Governance: the domain rulebook
The governance document is domain-scoped and is loaded by every participant, so all participants in a domain must have byte-identical, correctly signed governance. A trimmed example in the form SROS2 generates:
<dds xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://www.omg.org/spec/DDS-Security/20170801/omg_shared_ca_governance.xsd">
<domain_access_rules>
<domain_rule>
<domains><id>0</id></domains>
<allow_unauthenticated_participants>false</allow_unauthenticated_participants>
<enable_join_access_control>true</enable_join_access_control>
<rtps_protection_kind>SIGN</rtps_protection_kind>
<discovery_protection_kind>ENCRYPT</discovery_protection_kind>
<liveliness_protection_kind>SIGN</liveliness_protection_kind>
<topic_access_rules>
<topic_rule>
<topic_expression>*</topic_expression>
<enable_discovery_protection>true</enable_discovery_protection>
<enable_liveliness_protection>false</enable_liveliness_protection>
<enable_read_access_control>true</enable_read_access_control>
<enable_write_access_control>true</enable_write_access_control>
<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>
The values worth reading carefully are these. allow_unauthenticated_participants=false makes the domain reject any participant that cannot authenticate; setting it to true is a downgrade attack surface. enable_join_access_control forces a permissions check to join at all. discovery_protection_kind controls whether the discovery metadata (who publishes what) is hidden from eavesdroppers; encrypting it hides the graph from a passive observer, but it costs CPU at startup. The rtps_protection_kind setting protects whole RTPS messages including headers and is the main lever for packet-level integrity. The topic rule’s data_protection_kind and metadata_protection_kind choose per topic whether payloads are encrypted, merely signed, or sent in the clear.
Notice the topic expression *: a single rule for all topics. SROS2 default generation is intentionally strict on the data path, but you can write multiple rules with topic globs, and DDS-Security applies the first matching rule. A common production pattern is ENCRYPT for camera and LiDAR topics that may carry sensitive scenes and SIGN for high-rate control topics where integrity matters more than secrecy and CPU is tight.
Permissions: the per-subject grant
The permissions document grants rights to one certificate subject. Here is the shape SROS2 produces, abbreviated:
<permissions>
<grant name="/robot1/navigation">
<subject_name>CN=/robot1/navigation</subject_name>
<validity>
<not_before>2026-11-01T00:00:00</not_before>
<not_after>2027-11-01T00:00:00</not_after>
</validity>
<allow_rule>
<domains><id>0</id></domains>
<publish>
<topics>
<topic>rt/cmd_vel</topic>
<topic>rt/plan</topic>
</topics>
</publish>
<subscribe>
<topics>
<topic>rt/scan</topic>
<topic>rt/odom</topic>
</topics>
</subscribe>
</allow_rule>
<default>DENY</default>
</grant>
</permissions>
Two details trip people up. First, DDS topic names carry a prefix: rt/ for ROS topics, rq/ and rr/ for service requests and replies. Actions and parameters are built from those same topics and services, so they need matching entries, for example rq/.../_action/send_goalRequest for an action goal service. Hand-written policies that forget the rt/ prefix will deny everything and look correct. Second, <default>DENY</default> is the safe fallback, and the entire value of the file is in what you leave out.
Generating policy from observation
Writing permissions by hand for a hundred topics is painful, so SROS2 offers a policy format that is simpler than raw DDS XML and a generator. The idea is to write a short YAML policy that lists, per enclave, the topics, services and actions it may use, then compile it to the full XML. A minimal YAML example:
enclaves:
/robot1/navigation:
profiles:
ros_discovery_info:
subscribers: [ros_discovery_info]
publishers: [ros_discovery_info]
nav:
topics:
cmd_vel: {publishers: ALLOW}
scan: {subscribers: ALLOW}
services:
global_costmap/get_costmap: {requesters: ALLOW}
You then generate the XML artifacts:
ros2 security create_permission $ROS_SECURITY_KEYSTORE /robot1/navigation policy.yaml
The tooling also includes a policy generator that runs your system and records the topics it observes (ros2 security generate_policy), producing a first-draft YAML you then prune. This observation-driven approach is a good workflow in practice, with one caution: observing a running system records what it does today, not what it should be allowed to do. A node that is compromised and publishing extra topics during your recording session gets those topics baked into the policy. Generate in a clean, trusted lab, then review the diff by hand.
Why the files are signed
Both governance and permissions are signed using S/MIME (CMS) with the permissions CA, producing .p7s files. The reason is trust distribution. Every participant is configured with the permissions CA certificate and verifies the signature of both documents at startup. This means an attacker who can edit permissions.xml on a robot’s disk cannot extend its privileges without the permissions CA private key. It also means the policy files are portable: you can sign them centrally and ship them anywhere.
The signature is over the XML bytes exactly. Re-saving the file in an editor that changes line endings, or running it through a pretty-printer, invalidates the signature. The failure message is typically a vague parse or verification error. The fix is always to regenerate from source and re-sign, never to hand-edit a .p7s. The equivalent manual operation is an OpenSSL S/MIME sign, which you can reproduce for CI:
openssl smime -sign -text -in permissions.xml -out permissions.p7s \
-signer permissions_ca.cert.pem -inkey permissions_ca.key.pem
Handshakes, Certificates and Operating the PKI
With the files understood, the next question is what happens on the wire and how you operate the certificate lifecycle over years, not days.

Figure 3: Secure discovery between two participants. Identity and permissions are validated before any application data flows; a failure at any step means the peers never match.
What happens at discovery
When two participants meet, each announces itself through the (protected) participant discovery. The authentication plugin then validates the peer’s identity certificate against the configured identity CA, checks the validity window and runs the handshake, proving key possession and agreeing on a shared secret. Next, the access-control plugin validates the peer’s permissions document: signature against the permissions CA, validity dates, and a match between the subject name in the grant and the subject in the identity certificate. Only then does the cryptography plugin exchange the key material used to protect subsequent traffic. Endpoint matching, meaning topic by topic, is subject to a further permissions check: a publisher is matched with a subscriber only if both ends are allowed by the other side’s grant and the governance rules.
The practical consequence is that failures are sequential and each stage has its own symptom. A failure at authentication looks like total silence between two nodes. A failure at access control may let nodes see each other’s participants but refuse to match particular topics. A failure in cryptography is rare but shows up as samples that arrive and are dropped. The debugging section maps these symptoms to checks.
Certificate authorities and trust boundaries
SROS2 by default creates separate identity and permissions CAs (plus a shared self-signed ca used to chain them in the tutorial layout). Separation matters. The identity CA vouches for who a participant is; the permissions CA vouches for what a participant may do. If you keep them under different custodians or different HSMs, compromising the identity CA does not let an attacker mint arbitrary permissions, and compromising the permissions CA does not let them impersonate a node.
The default keystore is a development convenience. Its CA private keys are unencrypted files in private/. For production, treat the keystore directory as three different things: the CA private keys (offline, ideally in an HSM or a hardware-backed signer), the signed public artifacts (distributable freely), and the per-enclave private keys (secret, one per enclave, ideally never leaving the device that uses them). Conflating these is the single most common SROS2 security mistake I see in repositories: the entire keystore, CA keys included, checked into the same Git repository that is baked into every robot image. Anyone with one robot owns the fleet.
Where do per-robot private keys come from?
There are two sound patterns. In the central generation pattern, a provisioning host holds the CA keys, generates each robot’s enclave keys and certificates, and delivers the bundle over an authenticated channel. This is simple and matches the SROS2 tooling, but the private key exists on the provisioning host at some point. In the device-side CSR pattern, the robot generates its own key pair (ideally inside a TPM, secure element or at least a root-only file), sends a certificate signing request to the CA service, and receives a signed certificate. The private key never travels. SROS2 tooling does not provide the CSR flow out of the box in the form most fleets need, so teams write a thin script around OpenSSL, or integrate an existing PKI such as a cloud private CA or an open-source service. The DDS vendors accept standard PEM and PKCS#11-style URIs in some cases (vendor-dependent, so verify against your DDS), which is how hardware-backed keys can be used without code changes.
Validity, rotation and revocation
Certificates and permissions have not_before and not_after dates. The default SROS2 lifetimes are long (on the order of years in the generated artifacts; check your installed version) because the tooling is aimed at development. In production, shorter lifetimes (weeks to months) combined with automated renewal are your revocation mechanism, and this is not a fallback: the built-in DDS-Security authentication plugin supports a certificate revocation list (CRL) property, but support across vendors and the tooling to distribute CRLs are inconsistent. Do not design a fleet whose security depends on live revocation working. Design it so that a stolen robot’s credentials die at the next expiry, and so that the cost of re-issuing is low.
Rotation has one important twist: the clock. Validity checking uses the robot’s system time. A robot with a dead RTC battery that boots in 1970 will reject its own certificate as not yet valid. A robot whose clock is set far ahead by an attacker, or by a bad NTP source, can accept expired credentials or reject valid ones. Time sync is part of your security boundary, which is why serious fleets use authenticated time sources or at least monitor skew.
Performance Overhead: What You Pay
Everyone asks how much security costs. The honest answer is that it depends on payload size, message rate, protection kind, CPU instruction support and the DDS vendor, and I will not quote a single universal percentage. The mechanisms, however, are well understood, and you can reason about them.
Fixed costs at startup. Each participant pair performs a handshake: certificate chain validation, ECDH key agreement, signature verification of permissions. With dozens of participants, discovery storms can take noticeably longer than in an unsecured system, because each new pair handshakes independently. On a robot with 30 processes, that is hundreds of pairwise authentications in the worst case. This is a latency to first message, not a steady-state cost, but it matters for fast restart and for hot-plug devices.
Per-sample costs. With ENCRYPT, each sample is processed with AES-GCM, adding a cryptographic header and authentication tag (on the order of tens of bytes per sample) and CPU time roughly linear in payload size. On CPUs with AES instructions (AES-NI on x86, ARMv8 crypto extensions), AES-GCM throughput is high enough that a 1080p camera stream is not the bottleneck; on a small ARM core without those extensions, it can be. With SIGN, only a GMAC is computed, which is cheaper. The per-sample header also affects fragmentation: a payload that previously fit exactly into a UDP datagram may now fragment, and fragmentation interacts badly with lossy Wi-Fi.
Large messages and zero-copy. Security and shared-memory or loaned-message transports interact. Zero-copy paths in Fast DDS shared memory or Cyclone’s iceoryx integration cannot hand a buffer from publisher to subscriber without touching it if the data must be encrypted, so enabling ENCRYPT on a high-bandwidth topic generally forfeits zero-copy for that topic. Check your vendor’s documentation for the exact policy; behaviour differs, and some combinations are simply unsupported. A common design compromise is to run image pipelines inside one trusted enclave and process boundary using intra-process communication (which never touches DDS), and to cross security boundaries only with compact, derived data.
How to measure it yourself. Because published benchmarks vary with hardware and version, measure on your target. A sound methodology is to run ros2 topic hz and ros2 topic bw plus a latency probe (a publisher stamping messages and a subscriber computing the delta using a synchronised clock) across three configurations: security off, SIGN, and ENCRYPT, with identical QoS, at your real payload sizes and rates. Record CPU with pidstat and tail latency (p99), not just the mean. Expect the largest relative impact on small, high-rate messages (where the fixed per-sample header dominates) and on weak CPUs, and the smallest on large messages on modern hardware. Any single “x% overhead” figure you read without that context should be treated as illustrative.
A final performance point is easy to miss: security makes discovery chattier and stricter. Misconfigured permissions that cause repeated failed handshakes can generate steady log noise and CPU load. A system that is “slow after enabling security” is often a system that is retrying authentication, not one that is slow at encryption. Look at logs before reaching for a profiler.
Provisioning Fleets and CI
A secure design that is painful to provision will be bypassed. The goal is a pipeline where adding a robot or enclave is a command, not a ritual.

Figure 4: A fleet pipeline. CA keys stay on an offline provisioning host; per-robot bundles are delivered over a trusted channel; the fleet WAN link uses transport-level mTLS rather than exposing DDS.
Treat policy as code, keys as secrets
The policy YAML, the list of enclaves per robot role, and the generation scripts belong in version control. CA keys and enclave private keys do not. A clean separation: the repository contains the policy source and a provision.sh or Python tool; the CI system holds the permissions CA signing capability only inside a protected job (or calls out to a signing service); and the artifacts that come out of CI are the public, signed .p7s files, which are safe to distribute broadly.
A CI pipeline can usefully run the following stages on every pull request that touches policy:
# 1. Validate policy source
ros2 security create_keystore /tmp/ci_ks
ros2 security create_enclave /tmp/ci_ks /robot/navigation
ros2 security create_permission /tmp/ci_ks /robot/navigation policy.yaml
# 2. Verify signatures round-trip
openssl smime -verify -in /tmp/ci_ks/enclaves/robot/navigation/permissions.p7s \
-CAfile /tmp/ci_ks/public/permissions_ca.cert.pem -out /dev/null
# 3. Run an integration test with enforcement on
ROS_SECURITY_ENABLE=true ROS_SECURITY_STRATEGY=Enforce \
ROS_SECURITY_KEYSTORE=/tmp/ci_ks \
colcon test --packages-select my_nav_tests
The point of stage 3 is the one that saves you in production: your integration tests should run with Enforce on, against a throwaway keystore, so that a missing topic in the policy breaks CI instead of breaking a robot. Combine that with a negative test that launches a node under an unauthorised enclave and asserts that it cannot publish. Security tests that only prove the happy path give false comfort.
Delivering credentials to robots
For a small fleet, scp of an enclave bundle at imaging time is acceptable. For a larger fleet, use whatever your organisation already trusts for secrets: a device-bound enrolment token that exchanges for a certificate on first boot, a secrets manager, or a configuration-management agent. Whatever the method, put the enclave directory on a path readable only by the user running the ROS processes, with private keys at mode 0600. If your robots run containers, mount the enclave directory read-only as a volume rather than baking keys into the image, since an image that contains a private key is a key that exists on every registry mirror and every developer laptop that pulled it.
Because enclaves are per-subsystem, an orchestrator needs to map containers to enclaves. In Docker or Kubernetes terms, each pod gets only the enclave directories for its own nodes, and ROS_SECURITY_ENCLAVE_OVERRIDE (or the -e argument) selects the identity. This keeps the blast radius of a compromised container to its own grants. For micro-ROS devices that bridge via an XRCE-DDS agent, remember that the constrained device does not itself speak DDS-Security; the agent is the DDS participant and holds the credentials, so the agent host becomes a high-value target. We cover this architecture in our micro-ROS and XRCE-DDS agent tutorial.
Crossing the WAN
DDS discovery over multicast is not meant for wide-area links, and exposing a DDS domain to the internet, even secured, enlarges your attack surface to every bug in the vendor’s discovery stack. The usual pattern is to keep DDS-Security for the on-robot or on-site LAN and bridge to the cloud with a transport designed for it: a TLS-based router such as Zenoh, an MQTT bridge with mTLS, or a VPN overlay. That leads naturally to the next section.
The Zenoh Alternative: Transport-Level Security
rmw_zenoh_cpp replaces DDS with Eclipse Zenoh, a pub/sub and query protocol with routers. Its security is architecturally different and, for fleets, often simpler to operate. According to the rmw_zenoh README, security is available through access control, authentication and encryption, and a companion package, zenoh_security_tools, includes a script that generates Zenoh configuration with security set up. Configuration files are selected with ZENOH_ROUTER_CONFIG_URI and ZENOH_SESSION_CONFIG_URI, with ZENOH_CONFIG_OVERRIDE applying key-value changes afterwards.
Zenoh’s security features, from the Zenoh project’s own documentation, are TLS and mTLS at the transport level and an access-control rule system that matches on interfaces, certificate common names, key expressions and message type (publish, subscribe, query, reply), with a default allow or deny. I did not verify the exact config keys in the ROS packaging this session, so treat the specifics as something to confirm in the zenoh_security_tools documentation for your distribution. The conceptual mapping is clear enough to compare.
| Aspect | SROS2 with DDS-Security | rmw_zenoh with TLS and ACL |
|---|---|---|
| Identity | X.509 per participant (enclave) | X.509 per session or link, mTLS |
| Authorisation | Signed permissions.xml per subject | ACL rules in Zenoh config |
| Policy signing | S/MIME CMS signed by permissions CA | Config file integrity is your responsibility |
| Encryption scope | Per-topic ENCRYPT or SIGN, end to end between participants | Link-level TLS hop by hop, via routers |
| Topology | Peer-to-peer discovery, optional multicast | Router-centred, WAN friendly |
| Maturity in ROS | Years of use, multi-vendor | Newer, evolving with distributions |
| Policy tooling | ros2 security command family |
zenoh_security_tools script |
The decisive difference is trust scope. In DDS-Security the data is protected end to end between publisher and subscriber participants: an intermediate component, such as a routing bridge that forwards bytes, cannot read encrypted payloads. In Zenoh with TLS, each link is protected, and routers terminate the TLS session, so a router sees plaintext unless you add application-level encryption. For a fleet where routers are yours and well protected, link-level security is acceptable. For a multi-tenant deployment where the broker is operated by a third party, it is not, and you need payload encryption on top.
Which should you choose? My view is that the two are complementary rather than rival. Use DDS-Security (or simply an isolated, firewalled LAN) inside a robot or cell where peer-to-peer discovery and per-topic least privilege pay off, and use Zenoh mTLS plus ACLs on the WAN link where routers, NAT and flaky cellular dominate. If you run rmw_zenoh everywhere, you replace the ros2 security workflow with a Zenoh configuration workflow, and the lessons in this post about policy as code, short-lived credentials and negative tests carry over unchanged. Our Zenoh ROS 2 bridge tutorial walks through the bridge pattern in detail.
Common Failures and How to Debug Them
SROS2 failures are notorious for their silence: nodes simply do not see each other, and nothing explains why. A disciplined approach beats guessing. Work through the stages in order.
Step zero: confirm security is actually on
Print the environment in the exact shell or container that runs the node: env | grep ROS_SECURITY. Remember that ROS_SECURITY_ENABLE must be exactly true, that ROS_SECURITY_STRATEGY must be exactly Enforce to fail closed, and that the keystore path must contain enclaves/<enclave-name>. Run with ROS_SECURITY_STRATEGY=Enforce while debugging so that a missing enclave is a loud error rather than a silent downgrade. The most common “it doesn’t work” cause is an environment variable that was set in one terminal and not in the systemd unit, the launch file’s environment or the container.
Symptom table
| Symptom | Likely cause | Check |
|---|---|---|
| Node starts, sees no peers | Authentication failure, different CAs, wrong enclave | Compare identity_ca.cert.pem hashes across enclaves |
| Node exits with “unable to find security directory” | Enclave name does not match folder path | ls $ROS_SECURITY_KEYSTORE/enclaves; check the -e argument |
| Peers visible but topic does not match | Permissions deny, missing rt/ prefix |
Inspect permissions.xml, regenerate with policy tool |
| Signature or parse error at startup | .p7s stale or file edited after signing |
Re-sign; verify with openssl smime -verify |
| Worked yesterday, fails today | Certificate or permissions expired, clock skew | openssl x509 -noout -dates; check date on both hosts |
| Works on one host, not another | Different governance bytes or CA copies | Checksum all governance.p7s and CA files |
| Services fail, topics work | Missing rq/ and rr/ entries |
Add requesters and repliers in policy |
| Parameters or actions fail | Hidden topics not granted | Grant the _action and parameter service entries |
| Intermittent loss only with security on | UDP fragmentation from header growth | Reduce payload or tune MTU and fragmentation |
Tools for inspection
Use OpenSSL to read the artifacts directly. To see a certificate’s subject and validity, run openssl x509 -in cert.pem -noout -subject -dates. To confirm the chain, run openssl verify -CAfile identity_ca.cert.pem cert.pem. To read the signed permissions as text, run openssl smime -verify -in permissions.p7s -CAfile permissions_ca.cert.pem -text; if this fails, the node will fail too, and you have reduced the problem from “ROS is broken” to “this file is bad”. Compare the subject in the certificate to the subject_name in the grant character for character; whitespace and attribute order in the distinguished name are significant.
For network-level confirmation, capture with Wireshark. DDS-Security traffic is recognisable: with rtps_protection_kind set to SIGN or ENCRYPT, RTPS messages carry secure submessages, and with discovery protection you will see handshake messages (the DCPSParticipantStatelessMessage and volatile secure topics) flowing before anything else. Seeing handshake traffic but no data points toward permissions or crypto; seeing no handshake points toward network, multicast, domain ID or QoS-level issues unrelated to security.
Vendor-specific logging helps. Fast DDS exposes a security logging configuration and verbose log categories, Cyclone DDS has a tracing config with categories for security, and Connext has security logging properties. The exact property names differ per vendor and version, so consult the vendor documentation rather than a blog post, including this one.
Gotchas that deserve names
The shell-versus-service gap. Keys readable by your login user are not readable by the service account that launches the robot. Permission-denied reads of key.pem produce failures that look like authentication bugs.
Domain mismatch. Permissions are domain-scoped. A grant for domain 0 does nothing on domain 42, and ROS_DOMAIN_ID differences between environments cause denials that look like missing topics.
Mixed RMW vendors. Two nodes on different DDS vendors can interoperate unsecured in many cases, but secured interoperability depends on both implementations honouring the same plugin properties and cipher expectations. Test cross-vendor security explicitly before depending on it, and prefer a single RMW across the fleet.
Launch files that hide the enclave. In ROS 2 launch, an enclave can be set per node; composable-node containers share one participant and thus one enclave. If you load a navigation node and a teleop node into the same container, they share privileges. That may be exactly what you do not want.
Wildcards. The most dangerous permissions file is the one a developer got tired of debugging and filled with *. Lint policies in CI to reject wildcard topics outside a small allow-list.
A Threat Model for Robot Fleets
Security work should start from threats, not from features. A pragmatic model for a fleet of mobile robots has four asset groups: physical safety (the robot’s motion), operational integrity (missions, maps, parameters), data confidentiality (sensor data, floor plans, customer information) and availability (uptime). Attackers range from a curious insider with a laptop on the plant network to a remote actor who has compromised a cloud account, to someone with physical access to a stolen robot.
What SROS2 defends against
On the LAN, SROS2 defends against unauthenticated injection: a rogue machine publishing commands cannot join the domain without a certificate from your CA. It defends against passive eavesdropping when ENCRYPT and discovery protection are enabled. It defends against lateral privilege escalation among authenticated components, as far as your enclaves and permissions are fine-grained: a compromised perception node cannot publish cmd_vel if its grant forbids it. It also gives a cryptographic identity for audit, so you can say which enclave published, if you also instrument logging.
What SROS2 does not defend against
Be clear about the limits. It does not protect against a compromised host: once an attacker has root on the robot, they can read every enclave’s private key stored there and act as any enclave on that host. It does not protect the ROS graph above DDS, such as unauthenticated web bridges (rosbridge, which opens a WebSocket that can republish anything into DDS under its own identity). It does not by itself protect availability: an authenticated but malicious participant, or an unauthenticated flood to the discovery port, can still consume CPU and network, although authentication shortens the list of who can do so. It does not secure supply chain issues in your packages, nor the operating system, nor physical interfaces such as an open debug UART. And it does not give you forward-compatible revocation, as discussed.
Mapping controls to threats
| Threat | Control | Residual risk |
|---|---|---|
| Rogue device on plant network | Identity CA, allow_unauthenticated_participants=false |
Flooding of discovery ports |
| Eavesdropping on Wi-Fi | ENCRYPT plus discovery protection | Traffic analysis of size and timing |
| Compromised non-critical node | Per-enclave least-privilege permissions | Anything inside the same enclave |
| Stolen robot | Short-lived certs, TPM-bound keys, disk encryption | Window until expiry |
| Cloud account compromise | Separate WAN credentials, ACLs, no direct DDS exposure | Whatever the cloud role can command |
| Malicious policy change | Signed permissions, CI review, offline permissions CA | CA custodian compromise |
| Time manipulation | Authenticated time, skew monitoring | Spoofed GPS or NTP |
A short note on safety. Security controls can themselves cause safety incidents. A robot that fails closed on a certificate expiry in the middle of a shift is an availability and potentially safety problem if it stops in a doorway. Define a safe degraded behaviour for credential failure, such as controlled stop and local-only teleoperation, and test renewal long before expiry. Security and safety teams need to agree on this before deployment, not after the first outage. For safety-rated functions, the safety layer (hardware e-stop, safety PLC) should never depend on the ROS security configuration being correct.
Trade-offs and What Goes Wrong
SROS2 is real, working security, and it has real costs. The first is operational complexity. Every node needs an enclave decision, every enclave needs a policy, and every policy change needs re-signing and redistribution. Teams that do not automate this end up disabling security “temporarily” and never turning it back on. The second is opacity: failures are quiet and the tooling gives few diagnostics, so the debugging discipline above is not optional.
The third is vendor variance. DDS-Security is a standard, but features such as logging, CRLs, hardware-backed keys, and zero-copy interplay vary by implementation and version. A policy that works on Fast DDS may need adjustment on Cyclone or Connext. Pin versions and test the matrix you ship.
The fourth is the granularity trap. Too coarse, and permissions are meaningless; too fine, and the policy is unmaintainable and every software update requires re-issuing. The workable middle is a policy generated from a declarative source and reviewed in CI, as described. The fifth is false assurance. Enabling encryption on topics while leaving rosbridge, SSH with shared passwords or an open parameter server exposed gives an auditor a green checkbox and an attacker an easy path.
An anti-pattern worth calling out is the one keystore for everything: all robots sharing identical enclave keys. It is simple, and it makes every robot interchangeable to an attacker; one extraction compromises all. Per-robot keys are the minimum, and per-enclave-per-robot keys are the target.
Practical Recommendations
Begin with a threat model on one page: who can reach your network, what a malicious publisher could do, and what the worst physical outcome is. That tells you whether DDS-Security inside the robot is needed, or whether a locked-down network and WAN-level mTLS suffice for now. Many fleets need both, because robots operate in shared spaces with untrusted visitors’ devices.
Adopt the stricter defaults early: governance with unauthenticated participants disallowed, join access control on, discovery protection on, ENCRYPT for sensor and map topics, SIGN where CPU is scarce. Keep the CA keys off the robots and, for production, off any internet-connected machine. Use short validity, automate renewal, and monitor expiry as a first-class alert, the same way you monitor disk space.
Make security part of the developer loop so that it does not become a gate at the end: a make secure-sim target that spins up the simulation with a throwaway keystore and Enforce, so that every engineer sees denials during development. Review the generated permissions the way you review code.
A checklist to close:
- Set
ROS_SECURITY_ENABLE=true,ROS_SECURITY_STRATEGY=EnforceandROS_SECURITY_KEYSTOREin every launch environment, including systemd and containers. - One enclave per process type; no wildcard topics outside a reviewed allow-list.
- CA keys offline; per-robot, per-enclave private keys, mode
0600, never in images. - Policy YAML in Git, signed
.p7sproduced by a protected CI job. - CI runs integration tests with
Enforceand a negative test for denial. - Certificates and permissions short-lived; expiry alerting; time-sync monitoring.
- No direct DDS exposure across the WAN; use mTLS routers or a VPN.
- A documented safe state for credential failure.
- Pin DDS vendor and ROS versions; re-test security on each upgrade.
Frequently Asked Questions
What is SROS2 and is it the same as DDS-Security?
They are related but not the same. DDS-Security is the OMG standard defining authentication, access control, cryptography and logging plugins. SROS2 is the ROS 2 layer of tools, conventions and RMW integration that creates keystores, certificates, enclaves and signed policy files and tells your DDS vendor where to find them. You use SROS2 to operate DDS-Security without writing vendor-specific configuration by hand.
Which environment variables enable ROS 2 security?
Set ROS_SECURITY_ENABLE=true, ROS_SECURITY_STRATEGY=Enforce and ROS_SECURITY_KEYSTORE=<path>. Optionally use ROS_SECURITY_ENCLAVE_OVERRIDE to choose the enclave. Enable must be exactly true, and strategy must be exactly Enforce to fail closed; anything else is permissive. Older tutorials mention ROS_SECURITY_ROOT_DIRECTORY and ROS_SECURITY_NODE_DIRECTORY, which are legacy names, so check your distribution’s docs.
What is an enclave in ROS 2?
An enclave is a named set of security artifacts, an identity certificate, private key and signed permissions, shared by one or more nodes in a process. It exists because DDS-Security operates on participants, not individual nodes. You choose the enclave at launch with --ros-args -e <name>, and the matching directory under <keystore>/enclaves/ supplies the files.
How much overhead does SROS2 add?
It depends on payload size, rate, protection kind, CPU crypto support and vendor, so no universal figure is honest. Expect a startup cost from pairwise handshakes, a small per-sample header and CPU cost with ENCRYPT, lower cost with SIGN, and loss of zero-copy transport on encrypted topics. Measure p99 latency and CPU on your hardware with security off, SIGN and ENCRYPT.
Can I use SROS2 across the internet?
You can, but you should not expose DDS discovery directly. Keep DDS-Security for the local network or robot and bridge to the cloud with a transport built for the WAN, such as Zenoh with mTLS and access-control rules, a VPN or an MQTT bridge with mutual TLS. Remember that a TLS-terminating router sees plaintext unless you add payload encryption.
Why do my secured nodes not see each other?
Work through the stages. Confirm security variables in the exact process environment, then that both nodes use certificates from the same identity CA, that certificates and permissions have not expired or been affected by clock skew, that the permissions file signature verifies with OpenSSL, that subject names match, and that topic names include the rt/ prefix. Run with Enforce so missing enclaves fail loudly.
Further Reading
- Micro-ROS architecture, XRCE-DDS agent and microcontrollers: why the agent holds the credentials for constrained devices.
- Nav2 Lyrical vs Kilted: MPPI and the trajectory validator: the navigation traffic you will want to protect.
- Zenoh ROS 2 bridge tutorial: WAN bridging and transport-level security.
- SROS2 repository on GitHub: source, tutorials and policy format.
- OMG DDS Security specification: the standard behind the plugins.
- rmw_zenoh on GitHub: the Zenoh RMW and its configuration variables.
By Riju — about
