- The DDS layer.
theauth ros2 exportwrites the files SROS2 and DDS Security read, so the middleware refuses what the policy refuses. - A gate node. For things DDS cannot express (human approval, step up, rate limits, time windows, argument checks) a node asks theAuth before it publishes the command.
Resource names
The action
* means every verb of that kind. Names must be absolute (/scan, not scan or ~/scan) and made of letters, digits and underscores.
With the default matching, a * works only as a whole colon segment. ros2:/fleet/* is not a pattern: theAuth’s policy engine would never match it at runtime, so the exporter lists it as skipped instead of writing a DDS rule the runtime check disagrees with. List exact names, or turn on path matching (resourceMatching: "path" on the server, --resource-matching path on the export):
ros2:/fleet/**exports as the DDS globrt/fleet/*.ros2:/fleet/*(exactly one level) is skipped. A DDS permissions topic is an fnmatch pattern whose*also crosses/, so the grant would reach/fleet/a/bwhile theAuth denies it. This was checked against Fast DDS, see the glob result. Use**, list exact names, or pass--single-level widento export the wider DDS grant on purpose. The runtime gate still enforces one level.
Export to SROS2 and DDS Security
theauth login). To work offline, pass --permissions perms.json (an array of permissions, or an object with a permissions array) instead of --robot.
Four files are written:
The output is deterministic: sorted, no timestamps except the validity window you pass. Re-export after a policy change and
git diff shows what moved.
How permissions map to DDS
The mapping is the one SROS2 uses in itspermissions.xsl. A topic /cmd_vel becomes the DDS topic rt/cmd_vel. A service client publishes rq/<name>Request and subscribes to rr/<name>Reply, and the server does the reverse. An action client publishes the three _action request topics and subscribes to the three replies plus feedback and status. The grant ends in <default>DENY</default>, so anything not listed is refused.
DDS Security evaluates rules in document order and the first match wins. The exporter writes allow rules only, so order never changes the outcome.
The node baseline
A ROS 2 node opens a few endpoints of its own before your code runs: it publishes/rosout, publishes and subscribes /parameter_events, and answers six parameter services under its own name (describe_parameters, get_parameter_types, get_parameters, list_parameters, set_parameters, set_parameters_atomically). SROS2’s create_permission does not add these, and under Enforce a node missing any of them fails at startup (see what was verified). So the exporter adds them by default, for the node named by --node (default: the last token of the enclave path) in the namespace given by --namespace (default /).
The name must be the real ROS node name, because the parameter services are named after it. If the last enclave token is not a valid node name (for example robot-1, since node names cannot contain a hyphen) and you did not pass --node, the baseline is skipped and theauth-export.json says so. Pass --no-node-baseline to leave it out. Whether a node started with rosout logging and parameter services turned off still needs them was not tried.
Gated permissions
A permission with any constraint (requireApproval, requireAcr, maxCallsPerHour, timeWindow, ipAllowlist, allowedArgPatterns) cannot be written into a static DDS grant. Exporting it as a plain allow would drop the constraint without a word. So by default the exporter leaves it out and reports it under gated. The robot’s own enclave then cannot publish /cmd_vel at all.
That is the intended shape. Give the grant to the gate node instead, whose enclave is exported with --gated include, and let nothing else publish to the actuation topic:
--gated include on a normal participant is a weaker setup: anything holding that enclave’s key can publish without the runtime check. Use it only for the gate.
Sign and install
theAuth never sees your permissions CA key, so signing is yours. The usual route uses the ROS tooling, which also validates the policy against the schema:create_enclave first: create_permission reads the enclave certificate to set the validity window. The result is permissions.xml and permissions.p7s in the enclave directory. Note that this regenerates permissions.xml from policy.xml. The exported permissions.xml is the same content for a DDS stack without the ROS tooling, and it uses the validity window you gave to theauth ros2 export (--not-before, --not-after, default one year). Keep that window inside the enclave certificate’s.
To sign permissions.xml and governance.xml yourself, with OpenSSL (detached S/MIME, SHA-256, the same form ros2 security writes):
Enforce, ROS 2 falls back to permissive mode and an unlisted participant still starts. Each process needs its own enclave, because DDS Security grants are per participant.
The subject in permissions.xml has to equal the enclave certificate’s subject exactly, otherwise DDS ignores the grant. ros2 security create_enclave issues CN=<enclave path>, which is the default here. Pass --subject if yours differs.
Gate hazardous commands at runtime
The gate sits between whatever decides to move and the topic that moves the machine. It callsPOST /authorize/token with the robot’s agent token and publishes only on an explicit allowed: true.
allowed: true returns { ok: false } and the command does not run. Errors thrown by your own command function are not caught.
When the answer is approval_required, create the approval as described in Approvals, and pass the granted id as approvalId on the retry.
Two person rule for dangerous commands
Some commands should need two people, for example releasing a brake, moving an arm near a person or driving above a speed limit. Open the approval with a quorum, and the gate keeps answeringapproval_pending until two different operators have approved.
ttl runs out, the request expires and the first approval is discarded. Keep ttl short for robot commands so an old approval cannot be reused later. See Approvals for the full rules.
rclpy: a gate node
This node listens on/cmd_vel_request, asks theAuth, and republishes to /cmd_vel. Give the planner an enclave that may publish /cmd_vel_request only, and give this node the only grant for /cmd_vel. It uses only the Python standard library for HTTP.
/cmd_vel stops arriving, the motor controller should drop to zero velocity. Then a dead gate stops the robot instead of leaving the last command running.
rclcpp: the same idea, as a sketch
This is a sketch, not a tested package.http_post_json stands for whichever HTTP client you already link (libcurl, cpp-httplib). Run it off the executor thread, and treat every failure as a refusal.
Safety stops
An emergency stop must not wait for a network, a DDS graph, a process or theAuth. That belongs in hardware or a certified safety controller, under the standards for your machine (ISO 13850 covers emergency stop principles).safetyStops in guardRobotCommand exists for a narrower reason: a software stop command should not become slower, or refusable, because the auth server is down. Listed resources run immediately with no request. Matching is exact on purpose. ros2:*, a prefix, or a typo will not match, so the bypass cannot widen by accident. Keep the list to commands that only stop motion. Pass onSafetyStop to record that a bypass happened. It is never awaited and errors in it are swallowed.
Revocation shortens how long a stolen robot token or enclave key keeps working. It does not stop a robot that is already moving.
Compare two policies
Verified on ROS 2 Humble
Everything in this section was run, in Docker, on 2026-10-18. The setup: imageros:humble (digest sha256:566b87e6dfc4..., aarch64), ros-humble-sros2 0.10.9, ros-humble-rmw-fastrtps-cpp 6.2.10, Fast DDS 2.6.12, rclpy publisher and subscriber scripts on std_msgs/String, one enclave per process, ROS_SECURITY_ENABLE=true ROS_SECURITY_STRATEGY=Enforce. The exporter ran on the host and the XML was mounted into the container.
What this does not cover: only Humble and only rmw_fastrtps_cpp (no Jazzy, no Cyclone DDS, no Connext, no Zenoh). Only rclpy nodes, no rclcpp. Only topics. Services and actions were not exercised end to end, apart from the parameter services every node serves. Certificate expiry and key revocation were not tried. The gate node of this page was not run: the “gate” below is just an enclave exported with --gated include.
Export, then build the keystore
create_permission accepted every exported policy.xml.
What flowed and what was refused
Each row starts the subscriber, waits three seconds, then runs the publisher for twelve seconds at 2 Hz.
The refusal text from Fast DDS, as printed:
ROS_SECURITY_ENABLE=false) the same /secret publisher and subscriber exchanged messages (14 and 15 received in two runs), so the refusals above come from the permissions and not from the test scripts. And a publisher with security off talking to an Enforce subscriber on /chatter delivered 0 messages: the governance’s refusal of unauthenticated participants held.
Signing your own XML
The exportedpermissions.xml and governance.xml were also signed with the OpenSSL commands from Sign and install (using public/permissions_ca.cert.pem and private/permissions_ca.key.pem from the keystore), copied over the enclave’s permissions.* and governance.*, and openssl smime -verify printed Verification successful for all four. The talker and listener then behaved as in the first two table rows. openssl x509 -subject printed subject=CN = /w11/talker, and DDS accepted the exporter’s CN=/w11/talker as equal.
The node baseline is needed
With only the application topics inpolicy.xml, a node under Enforce does not start. Observed in order, each one fixed by adding that item:
/rosout, six parameter services and /parameter_events made the first table row work.
Globs
Enclaveglobss was exported from ros2:/fleet/** with --resource-matching path (DDS topic rt/fleet/*). Enclave glob1 was exported from ros2:/fleet/* with --resource-matching path --single-level widen, which writes the same rt/fleet/*. Both enclaves, creating publishers:
* of a permissions topic does cross /, as this page says: the one level /fleet/* cannot be written as a DDS glob, and --single-level widen really does grant the deeper names. A glob does not match its own prefix (/fleet).
What is not covered
- Zenoh. There is no Zenoh
access_controlexporter. rmw_zenoh’s key expressions carry more than the topic name, and a config that looks right but matches nothing is worse than none. Thezenoh_security_toolspackage from the rmw_zenoh repository generates Zenoh access control from an SROS2 policy file, so feed it the exportedpolicy.xml. - micro-ROS. A microcontroller does not run a full DDS participant. Check eProsima’s documentation for how the Micro XRCE-DDS agent and its client link are secured, and treat the agent as the trust boundary.
- Delegated permissions.
--robotexports the robot’s own permissions and the live permissions delegated to it (GET /agents/:id?include=delegated). - Certificate lifetimes. DDS Security validity windows are absolute dates. Pass
--enclave-cert <pem>and the grant window defaults to the certificate’s own window; a--not-beforeor--not-afteroutside it is refused. Re-export and re-sign on a schedule shorter than the window. - Per node policy inside one process. DDS Security grants apply per participant, and ROS 2 uses one participant per process, so two composed nodes share one grant.
Sources
- OMG DDS Security 1.1 (formal/18-04-01), permissions and governance documents.
- SROS2 source and schemas,
github.com/ros2/sros2(permissions.xsl,policy.xsd, default governance). - ROS 2 design article, “ROS 2 DDS-Security integration”.
- Zenoh manual, access control.