Skip to main content
A robot is an agent in theAuth. Its permissions say which topics it may publish and subscribe to, which services it may call or answer, and which actions it may send or execute. theAuth uses that one list in two places:
  1. The DDS layer. theauth ros2 export writes the files SROS2 and DDS Security read, so the middleware refuses what the policy refuses.
  2. 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.
theAuth is not a safety system and nothing here makes a machine compliant with ISO 10218, ISO/TS 15066 or IEC 62443. An emergency stop must be a hardware or safety controller function that works with the network down. See Safety stops.

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 glob rt/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/b while theAuth denies it. This was checked against Fast DDS, see the glob result. Use **, list exact names, or pass --single-level widen to export the wider DDS grant on purpose. The runtime gate still enforces one level.

Export to SROS2 and DDS Security

You need to be logged in as an admin (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 its permissions.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:
Run 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):
Then run the robot with security enforced:
Without 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 calls POST /authorize/token with the robot’s agent token and publishes only on an explicit allowed: true.
It fails closed. A timeout, a refused connection, a 500, a body that is not JSON, or anything other than 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 answering approval_pending until two different operators have approved.
The person who opened the request cannot be one of the two, and the same operator cannot approve twice. A single denial cancels it, and if the second operator does not act before 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.
Put a watchdog on the actuation side too: if /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

Prints the DDS topics that would be added or removed and the permissions that became or stopped being gated. Exit code 0 means no change, 2 means the exported policy differs, 1 is an error. Use it in review before rolling a policy change to a fleet.

Verified on ROS 2 Humble

Everything in this section was run, in Docker, on 2026-10-18. The setup: image ros: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:
Two more checks. With security switched off (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 exported permissions.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 in policy.xml, a node under Enforce does not start. Observed in order, each one fixed by adding that item:
That is why the node baseline exists. Exporting /rosout, six parameter services and /parameter_events made the first table row work.

Globs

Enclave globss 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:
So in Fast DDS the * 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_control exporter. rmw_zenoh’s key expressions carry more than the topic name, and a config that looks right but matches nothing is worse than none. The zenoh_security_tools package from the rmw_zenoh repository generates Zenoh access control from an SROS2 policy file, so feed it the exported policy.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. --robot exports 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-before or --not-after outside 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.
Last modified on October 19, 2026