Skip to content

feat(examples): ROS2 transport-level non-bypass hardening demo#174

Open
ExpertVagabond wants to merge 1 commit intosint-ai:mainfrom
ExpertVagabond:feat/ros2-transport-hardening
Open

feat(examples): ROS2 transport-level non-bypass hardening demo#174
ExpertVagabond wants to merge 1 commit intosint-ai:mainfrom
ExpertVagabond:feat/ros2-transport-hardening

Conversation

@ExpertVagabond
Copy link
Copy Markdown
Collaborator

Closes #161.

Context

The existing examples/ros2-lifecycle-guard is explicitly application-level: a node voluntarily calls PolicyGateway.intercept() before acting. The demo's README already calls this out — any other process on the same DDS domain can still publish directly to /cmd_vel because nothing at the transport layer stops it.

This PR adds the transport-layer counterpart so the two demos together cover both rows of the enforcement matrix:

Layer Enforced by Catches Does not catch
Application PolicyGateway.intercept() Token/constraints/physical context/tier A different process publishing to DDS
Transport SROS2 enclaves + DDS Secure Any unauthenticated or unauthorized participant Application-semantic policy

New directory: examples/ros2-transport-hardening/

  • keystore-init.sh — one-shot that builds an SROS2 keystore and two per-identity enclaves (`/guarded_talker` → publish `rt/cmd_vel`; `/guarded_camera` → subscribe `rt/camera/front`), overlays custom `permissions.xml`, and re-signs with the permissions CA using `openssl smime`.
  • `guarded_talker.py` — reference defender. Calls `PolicyGateway.intercept()` before publishing AND runs under `ROS_SECURITY_ENABLE=true ROS_SECURITY_STRATEGY=Enforce` with a matching enclave.
  • `attacker.py` — reference bypass attempt. Same network + `ROS_DOMAIN_ID`, Enforce mode, but no keystore. rmw either refuses participant creation (exit `3`) or DDS Secure discovery never matches it to the guarded subscriber (exit `2`). Exit `0` would indicate a regression.
  • `conformance-check.sh` — CI-friendly orchestrator: waits for the `[guarded_talker] OK` sentinel, then runs the attacker container and asserts the exit code. Fails loudly if a bypass publish reaches the guarded topic.
  • `docker-compose.yml` / `Dockerfile` — full stack: gateway + postgres + redis + keystore-init + guarded-talker + attacker (profile-gated so conformance runs it via `docker compose run`).
  • `README.md` — layered-model table, manual keystore recipe, defense-in-depth pointers (`ROS_DOMAIN_ID` separation, NetworkPolicy on DDS ports, gateway-side endpoint allowlist), explicit limitations (no full middleware interceptor — matching the non-goal from ROS2: transport-level non-bypass hardening (SROS2/DDS containment) #161).

Also updates `examples/ros2-lifecycle-guard/README.md` to cross-link the new demo so the two are read as a pair.

Acceptance-criteria mapping (from #161)

  • Demo README includes exact commands for generating keys/enclaves and running the hardened setup.
  • A CI-friendly scripted check demonstrates that a bypass publish fails (`conformance-check.sh`).
  • Docs clearly separate application-level guard vs transport-level enforcement (layered-model table + cross-links).

Test plan

  • `bash -n` on both shell scripts; `python3 -m py_compile` on both Python files; YAML lint on compose file — all clean.
  • Maintainer-side (requires Docker + ~2GB pull for `ros:humble-ros-base`):
    ```bash
    cd examples/ros2-transport-hardening
    ./conformance-check.sh
    ```
    Expected: `[conformance] PASS: attacker blocked at transport (exit 2|3)`.

Non-goals honored

A full ROS 2 middleware interceptor (a hard MITM the node cannot route around) is out of scope per the issue; this PR only covers the SROS2/DDS-Security recipe and conformance test.

…sint-ai#161)

The existing examples/ros2-lifecycle-guard is explicitly application-level:
code outside the guarded node can still publish to /cmd_vel directly. This
adds the transport-level counterpart so the enforcement story is
non-bypassable at the DDS boundary, not just inside the guarded process.

New examples/ros2-transport-hardening/:

- keystore-init.sh: builds an SROS2 keystore, creates per-identity
  enclaves (/guarded_talker publish rt/cmd_vel, /guarded_camera subscribe
  rt/camera/front), overlays custom permissions.xml, and re-signs with
  the permissions CA.
- guarded_talker.py: reference defender. Calls PolicyGateway.intercept()
  before every publish AND runs under ROS_SECURITY_ENABLE=true
  ROS_SECURITY_STRATEGY=Enforce with a matching enclave.
- attacker.py: reference bypass attempt. Same network + ROS_DOMAIN_ID,
  Enforce mode, but no keystore — DDS Secure refuses participant
  creation or subscriber matching. Exit codes 2/3 indicate blocked, 0
  indicates a regression.
- conformance-check.sh: CI-friendly orchestrator that asserts the
  attacker is blocked (exits 2 or 3) and fails if a bypass publish
  reaches the guarded topic.
- docker-compose.yml / Dockerfile: gateway + postgres + redis +
  keystore-init one-shot + guarded-talker + attacker (profile-gated so
  conformance-check runs it explicitly via 'docker compose run').
- README.md: layered-model table (application vs transport), manual
  keystore recipe, optional defense-in-depth (ROS_DOMAIN_ID separation,
  network policies, gateway-side endpoint allowlist), explicit
  limitations.

Also updates examples/ros2-lifecycle-guard/README.md to cross-link this
demo so the two can be read together.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ROS2: transport-level non-bypass hardening (SROS2/DDS containment)

2 participants