From 0fd3f8b5351700a78d13586600f96c7a62d4a1df Mon Sep 17 00:00:00 2001 From: Jose Maria Lopez Vega Date: Sun, 21 Jun 2026 20:01:31 +0200 Subject: [PATCH 1/4] Updated doc. Changes to make SecureLogReader.xml RTI Spy-friendly. --- README.md | 25 ++++-- docs/CONTRIBUTING.md | 83 ++++++++++--------- docs/release/RELEASE_PLAN.md | 4 +- modules/02-record-playback/README.md | 2 + modules/04-security-threat/README.md | 2 + system_arch/qos/README.md | 23 +++++ system_arch/security/README.md | 3 +- .../SecureLogReader/SecureLogReader.xml | 6 ++ system_arch/xml_app_creation/README.md | 26 +++++- tests/README.md | 1 - tests/docker/README.md | 6 +- 11 files changed, 126 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 2354dd8..5d16184 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,6 @@ This repository contains documentation and module demo applications showcasing d - [Module 03: Remote Teleoperation with RTI Real-Time WAN Transport](#module-03-remote-teleoperation-with-rti-real-time-wan-transport) - [Module 04: Security Threat Demonstration](#module-04-security-threat-demonstration) - [Hands-On: Architecture](#hands-on-architecture) -- [Architecture Overview](#architecture-overview) -- [Hands-On: Architecture](#hands-on-architecture) - [Architecture Overview](#architecture-overview) - [Data Types](#data-types) - [Quality of Service (QoS)](#quality-of-service-qos) @@ -139,6 +137,12 @@ The RTI MedTech Reference Architecture demonstrates use cases and capabilities o Use the module-specific READMEs when you want to run a demo. They describe what each workflow launches, why it exists, and the exact `launch.py` commands to use from the repository root. +`launch.py` accepts either a single module (`python3 launch.py [apps ...] [-s]`) or a **predefined scenario** that launches a curated set of applications across modules in one command (`python3 launch.py --scenario [-s]`). Scenarios are declared in [`resource/config/scenarios.json`](./resource/config/scenarios.json) and cover common multi-component workflows (e.g. `record`, `replay`, `teleop-or-side`, `security-threat`). List them with: + +```bash +python3 launch.py --list-scenarios +``` + ### [Module 01: Digital Operating Room](./modules/01-operating-room/) ### [Module 02: RTI Recording Service & RTI Replay Service](./modules/02-record-playback/) @@ -220,10 +224,12 @@ This reference architecture defines the following QoS Profiles in [Qos.xml](./sy | Qos Library | Qos Profile | Intended Use | ----------- | --------- | ----------- | System | DefaultParticipant | Common, or base, *system configuration* (e.g. transport, network interfaces, discovery, thread priorities, etc.) +| System | WanConfig | WAN transport (UDPv4_WAN) configuration, used by Module 03: Remote Teleoperation | DataFlow | Streaming | Periodic data that is published at a high frequency (i.e. frequencies <1 second) | DataFlow | Status | "Current status"-like data, sent once at the beginning of operation and again only upon change to the status | DataFlow | Command | Data that represents commands or trigger some action in the system | DataFlow | Heartbeat | Assert and detect the presence of system devices +| DataFlow | SecureLog | Reliable, transient-local delivery for the DDS Security builtin secure-log topic ### Domains & Topics @@ -242,11 +248,12 @@ Legend: This reference architecture defines the following Domains in [DomainLibrary.xml](./system_arch/xml_app_creation/DomainLibrary.xml): -| Domain | Intended Use -| ------ | ----------- -| OperationalDataDomain | Real-time operational medical device data +| Domain | Domain ID | Intended Use +| ------ | --------- | ----------- +| OperationalDataDomain | 0 | Real-time operational medical device data +| SecureLogDomain | 0 | DDS Security builtin secure logging (consumed by the SecureLogReader participant) -*Note, this reference architecture defines just a single Domain. As a Connext system design scales over time, additional domains could be defined for monitoring, logging, etc. Those additional domains should not affect the performance of our operational data, and therefore should belong to a different domain.* +*Note, both domains are defined with the same Domain ID (`0`), so they currently resolve to the **same** DDS domain. `SecureLogDomain` is defined separately to keep the secure-logging configuration distinct, so that as a Connext system design scales over time, logging (and other concerns such as monitoring) can be moved to a different Domain ID without touching the operational data configuration. Isolating such data on a separate domain ensures it does not affect the performance of operational data.* This reference architecture defines the following Topics in [DomainLibrary.xml](./system_arch/xml_app_creation/DomainLibrary.xml): @@ -257,6 +264,7 @@ This reference architecture defines the following Topics in [DomainLibrary.xml]( | OperationalDataDomain | `t/DeviceHeartbeat` | Assert that a unique system component is alive | OperationalDataDomain | `t/DeviceCommand` | Command initiating a status (e.g. `START`, `SHUTDOWN`) to a unique system component | OperationalDataDomain | `t/Vitals` | Data representative of a unique patient's collected vital signs +| SecureLogDomain | `DDS:Security:LogTopicV2` | DDS Security builtin logging topic, carrying the `DDSSecurity::BuiltinLoggingTypeV2` type *Note, this reference architecture defines a unique Topic for each Data Type defined. While a Topic may only reference a single Data Type, a multi-purpose Data Type can be associated with multiple Topics. It is a **best practice** to limit the number of defined Topics, but in doing so, it may be feasible to reuse a Data Type for several Topics.* @@ -302,6 +310,7 @@ This reference architecture defines the following DomainParticipants in [Partici | OperationalDataDomain | Orchestrator | `t/DeviceStatus`, `t/DeviceHeartbeat` | `t/DeviceCommand` | Administer device-level commands and monitor presence and status of all devices. | OperationalDataDomain | PatientSensor | `t/DeviceCommand` | `t/Vitals`, `t/DeviceStatus`, `t/DeviceHeartbeat` | Stream simulated patient vitals. | OperationalDataDomain | PatientMonitor | `t/DeviceCommand`, `t/Vitals` | `t/DeviceStatus`, `t/DeviceHeartbeat` | Process and display patient vitals. +| SecureLogDomain | SecureLogReader | `DDS:Security:LogTopicV2` | -- | Subscribe to the DDS Security builtin secure-log topic. *Note, this reference architecture utilizes one DomainParticipant for each device application. It is a **best practice** to define one DomainParticipant per application. However, in more complex systems, an application may be required to operate on multiple Domains. This requires defining multiple DomainParticipants for those applications that run in parallel.* @@ -315,8 +324,8 @@ The reference architecture configures security in [SecureAppsQos.xml](./system_a | Component | Security Features | ---------------------- | ----------------- -| **LAN Communications** | Domain 0 governance, participant-specific certificates and permissions -| **WAN Communications** | Domain 1 governance for WAN connections +| **LAN Communications** | `OperationalDomain` governance, participant-specific certificates and permissions +| **WAN Communications** | `TeleopWanDomain` governance for WAN connections (Module 03), including PSK-protected RTPS | **RTI Services** | Dedicated security profiles for Recording/Replay Services and Routing Services Security Artifacts Structure in [security](./system_arch/security/): diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 0b5a3ac..e3a81ed 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -87,17 +87,28 @@ the commit is recorded: | Hook | What it checks / fixes | | --- | --- | -| `ruff` | Python lint — auto-fixes where possible | +| `ruff-check` | Python lint — auto-fixes where possible | | `ruff-format` | Python formatting — auto-reformats files | | `trailing-whitespace` | Removes trailing whitespace from all text files | | `end-of-file-fixer` | Ensures files end with a single newline | -| `check-yaml` | Validates YAML syntax | +| `check-yaml` | Validates YAML syntax (excludes `.clang-format`) | +| `check-json` | Validates JSON syntax | +| `check-toml` | Validates TOML syntax | | `check-xml` | Validates XML syntax | +| `check-merge-conflict` | Blocks committed merge-conflict markers | +| `check-case-conflict` | Blocks names that collide on case-insensitive filesystems | +| `check-illegal-windows-names` | Blocks filenames invalid on Windows | +| `check-executables-have-shebangs` / `check-shebang-scripts-are-executable` | Keep executable bits and shebangs consistent | +| `detect-private-key` | Blocks accidentally committed private keys | +| `requirements-txt-fixer` | Normalizes `requirements*.txt` ordering | +| `mixed-line-ending` | Enforces LF (CRLF for batch files) | +| `name-tests-test` | Enforces `test_*` naming for pytest files | | `check-added-large-files` | Blocks files larger than 500 KB | | `codespell` | Checks spelling for source and docs using `pyproject.toml` settings | | `clang-format` | Reformats C/C++ source files | -| `rumdl` | Markdown lint + auto-fix | -| `rumdl-fmt` | Markdown formatting pass | +| `rumdl` / `rumdl-fmt` | Markdown lint + auto-fix and formatting pass | + +See `.pre-commit-config.yaml` for the authoritative hook list and configuration. If a hook **modifies files**, the commit is aborted. Re-stage the modified files and commit again: @@ -123,22 +134,17 @@ committing. In exceptional circumstances you can bypass hooks with ### CI pipeline (`.github/workflows/ci.yml`) +The pipeline has two jobs; `test` runs only after `lint` passes: + | Job | What it does | | --- | --- | -| Lint & Format | `ruff check`, `ruff format --check`, codespell, clang-format dry-run, markdown lint via `rvben/rumdl` action | -| Build | CMake configure + build all C++ modules | -| Project-level Tests | `pytest tests/` | -| Unit Tests | Fast Python type/script/QoS tests | -| DDS Communication Tests | Non-GUI DDS pub/sub tests | -| Integration Tests | Slow end-to-end demo flow tests | -| GUI Tests | Headless Qt application tests | -| Security Tests | DDS Security plugin tests | -| Module 02 Tests | Record/Playback module tests | -| Module 04 Tests | Security Threat module tests | - -CI uses a pinned Ruff version (see [Upgrading Ruff](#upgrading-ruff)). If -pre-commit and CI share the same pin, a clean local commit will not produce -lint failures in CI. +| `lint` (Lint & Format) | Runs **all** pre-commit hooks across the repo (`pre-commit/action` with `--all-files`) — the same ruff, ruff-format, codespell, clang-format, rumdl, and hygiene hooks you run locally. | +| `test` (Build & Test) | Installs Connext (apt) and Python deps, builds all C++ modules with `python build.py`, generates the system and Module 04 security artifacts, starts Xvfb, then runs the full suite from the repo root with `python -m pytest -v -m "not build_pipeline"` and uploads `results.xml`. | + +Because the `lint` job runs `pre-commit` itself, CI and your local hooks execute +the **exact same** hook versions (pinned in `.pre-commit-config.yaml`). A clean +local commit will therefore not produce lint failures in CI. See +[Upgrading Ruff](#upgrading-ruff) for how the Ruff pin is managed. ## Testing CI Locally with act (Optional) @@ -191,8 +197,9 @@ docker compose -f tests/docker/docker-compose.yml run --rm --build test \ modules/01-operating-room/tests/test_types.py -v ``` -> **Note:** The Docker default command runs `pytest -v` via the -> test entrypoint. It executes functional/behavioral tests. It does **not** run Ruff lint, +> **Note:** With no arguments, the Docker test entrypoint runs `pytest` over the +> whole repo (it passes any arguments you supply straight through to pytest). +> It executes functional/behavioral tests. It does **not** run Ruff lint, > rumdl markdown lint, or clang-format; those are enforced by pre-commit > (locally) and the CI lint job (on push/PR). @@ -228,37 +235,37 @@ Before opening a PR, verify: ## Upgrading Ruff -Ruff is pinned in two places so that local pre-commit hooks and CI produce -identical results. When upgrading, update **both together**: +The CI `lint` job runs `pre-commit`, so there is a **single** Ruff pin that +governs both local hooks and CI: the `astral-sh/ruff-pre-commit` revision in +`.pre-commit-config.yaml`. Bump that one pin to upgrade Ruff everywhere. -| File | Setting to change | -| --- | --- | -| `.github/workflows/ci.yml` | `pip install ruff==` | -| `.pre-commit-config.yaml` | `rev: v` under `astral-sh/ruff-pre-commit` | +| File | Setting to change | When | +| --- | --- | --- | +| `.pre-commit-config.yaml` | `rev: v` under `astral-sh/ruff-pre-commit` | Every Ruff bump (this is the authoritative pin used by CI). | +| `pyproject.toml` | `required-version = ">="` | Only when raising the minimum supported version. | +| `requirements-dev.txt` | `ruff>=` | Only when raising the minimum supported version. | -`pyproject.toml` carries a minimum version (`required-version = ">=0.15"`) and -`requirements-dev.txt` carries a matching lower bound (`ruff>=0.15`). These do -**not** need to change on every Ruff bump unless the new version introduces a -breaking change to the configuration format. +The `pyproject.toml` and `requirements-dev.txt` bounds are lower bounds for +developers who run Ruff outside pre-commit; they do **not** need to change on +every Ruff bump unless the new version introduces a breaking change to the +configuration format. ### Procedure ```bash -# 1. Update the two pinned locations (ci.yml and .pre-commit-config.yaml) - -# 2. Update your local pre-commit environment +# 1. Update the pin in .pre-commit-config.yaml pre-commit autoupdate --freeze # or manually set the rev -# 3. Run the full pre-commit suite to surface any new lint findings +# 2. Run the full pre-commit suite to surface any new lint findings pre-commit run --all-files -# 4. Fix any new violations introduced by the new Ruff version +# 3. Fix any new violations introduced by the new Ruff version -# 5. Update the minimum version bounds if appropriate +# 4. Update the minimum version bounds only if appropriate # pyproject.toml: required-version = ">=" # requirements-dev.txt: ruff>= -# 6. Commit everything together -git add .pre-commit-config.yaml .github/workflows/ci.yml pyproject.toml requirements-dev.txt +# 5. Commit everything together +git add .pre-commit-config.yaml pyproject.toml requirements-dev.txt git commit -m "chore: bump Ruff to " ``` diff --git a/docs/release/RELEASE_PLAN.md b/docs/release/RELEASE_PLAN.md index 7eced04..d158ffe 100644 --- a/docs/release/RELEASE_PLAN.md +++ b/docs/release/RELEASE_PLAN.md @@ -376,8 +376,8 @@ and update them as part of the release process. | Dependency | Current Minimum Version | Tracked In | | --- | --- | --- | -| RTI Connext DDS | 7.3.0 | CMakeLists.txt, README.md | -| RTI Code Generator | 4.3.0 | Generated source headers | +| RTI Connext DDS | 7.7.0 | README.md, `.github/workflows/ci.yml` | +| RTI Code Generator | Bundled with Connext 7.7.0 | Generated source headers | | CMake | 3.17 | CMakeLists.txt | **When a dependency version changes:** diff --git a/modules/02-record-playback/README.md b/modules/02-record-playback/README.md index 9076abd..f159c6f 100644 --- a/modules/02-record-playback/README.md +++ b/modules/02-record-playback/README.md @@ -48,6 +48,8 @@ Module-specific notes: ## Run the Demo > Important: Run the commands below from the repository root. `launch.py` lives at the project root and is the single runtime entrypoint for this project. +> +> Tip: the predefined `record` and `replay` scenarios bundle the operating room apps and the corresponding service into a single command (e.g. `python3 launch.py --scenario record`). The step-by-step flow below instead runs them in separate terminals so you can start and stop the service independently of the OR apps. Run `python3 launch.py --list-scenarios` to see all scenarios. ### 1. Run Operating Room Applications diff --git a/modules/04-security-threat/README.md b/modules/04-security-threat/README.md index 31b9f4e..be8d40b 100644 --- a/modules/04-security-threat/README.md +++ b/modules/04-security-threat/README.md @@ -87,6 +87,8 @@ Ensure the RTI Security Plugins are installed on every machine that will run a s ## Run the Demo > Important: Run the commands below from the repository root. `launch.py` lives at the project root and is the single runtime entrypoint for this project. +> +> Tip: the predefined `security-threat`, `threat-inject`, and `threat-exfiltrate` scenarios bundle the operating room apps with the threat application(s) in a single command (e.g. `python3 launch.py --scenario threat-inject`). The step-by-step flow below runs them in separate terminals so you can switch the OR between unsecured and secured modes independently. Run `python3 launch.py --list-scenarios` to see all scenarios. ### 1. Run Operating Room Applications diff --git a/system_arch/qos/README.md b/system_arch/qos/README.md index 629a787..a016270 100644 --- a/system_arch/qos/README.md +++ b/system_arch/qos/README.md @@ -10,11 +10,13 @@ This README describes how we've approached QoS in this reference architecture. F - [Qos.xml](#qosxml) - [SystemLibrary](#systemlibrary) - [SystemLibrary::DefaultParticipant profile](#systemlibrarydefaultparticipant-profile) + - [SystemLibrary::WanConfig profile](#systemlibrarywanconfig-profile) - [DataFlowLibrary](#dataflowlibrary) - [DataFlowLibrary::Streaming profile](#dataflowlibrarystreaming-profile) - [DataFlowLibrary::Status profile](#dataflowlibrarystatus-profile) - [DataFlowLibrary::Command profile](#dataflowlibrarycommand-profile) - [DataFlowLibrary::Heartbeat profile](#dataflowlibraryheartbeat-profile) + - [DataFlowLibrary::SecureLog profile](#dataflowlibrarysecurelog-profile) - [Application-specific QoS: NonSecureAppsQos.xml and SecureAppsQos.xml](#application-specific-qos-nonsecureappsqosxml-and-secureappsqosxml) - [XML QoS Best Practices](#xml-qos-best-practices) @@ -78,11 +80,21 @@ This reference architecture defines the following QoS Libraries in [Qos.xml](./Q | QoS Profile | Intended Use | ----------- | ------------ | [*DefaultParticipant*](#systemlibrarydefaultparticipant-profile) | Configuration common to all DomainParticipants. +| [*WanConfig*](#systemlibrarywanconfig-profile) | WAN transport configuration, used by Module 03: Remote Teleoperation. #### ***SystemLibrary::DefaultParticipant* profile** This QoS profile acts as a common base configuration for all DomainParticipants in the system to provide a level of consistency. It inherits from a builtin profile called *BuiltinQosLib::Generic.Common* through the `base_name` XML attribute. +#### ***SystemLibrary::WanConfig* profile** + +This QoS profile configures the DomainParticipant for communication over the Wide Area Network (WAN). It inherits from *BuiltinQosLib::Generic.Common* and additionally: + +- Enables the **UDPv4_WAN** builtin transport (via the `transport_builtin` mask), which provides the RTI Real-Time WAN Transport's NAT-traversal capabilities. +- Shortens the participant liveliness assert period to speed up discovery over the WAN. + +It is used by Module 03: Remote Teleoperation. See the [Module 03 README](../../modules/03-remote-teleoperation/README.md) for the WAN scenarios that rely on it. + ### ***DataFlowLibrary*** *DataFlowLibrary* contains the following QoS profiles (``): @@ -93,6 +105,7 @@ This QoS profile acts as a common base configuration for all DomainParticipants | [*Status*](#dataflowlibrarystatus-profile) | *RELIABLE* | *KEEP_LAST 1* | *TRANSIENT_LOCAL* | -- | "Current status"-like Topics, sent once at the beginning of operation and again only upon change to the status. | [*Command*](#dataflowlibrarycommand-profile) | *RELIABLE* | *KEEP_LAST 1* | *VOLATILE* | -- | Topics that transmit commands or trigger some action in the system. | [*Heartbeat*](#dataflowlibraryheartbeat-profile) | *BEST_EFFORT* | *KEEP_LAST 1* | *VOLATILE* | *200 ms* | To assert and detect the presence of system components. +| [*SecureLog*](#dataflowlibrarysecurelog-profile) | *RELIABLE* | *KEEP_LAST 64* | *TRANSIENT_LOCAL* | -- | Delivery of the DDS Security builtin secure-log Topic. #### ***DataFlowLibrary::Streaming* profile** @@ -141,6 +154,16 @@ Since this QoS profile uses *BEST_EFFORT* Reliability QoS, a minimal amount of s >**Best Practice:** Publish samples on Topics for which the DataWriter QoS defines a finite Deadline QoS period, at a rate that is 2x-4x that of the configured Deadline period. This ensures an infrequent drop in sample does not falsely trigger the `REQUESTED_DEADLINE_MISSED` status for DataReaders. +#### ***DataFlowLibrary::SecureLog* profile** + +This QoS profile is used by the *dr/SecureLog* DataReader (under the *dp/SecureLogReader* DomainParticipant) to consume the RTI Security Plugins builtin secure-logging Topic. + +It inherits from *BuiltinQosLib::Generic.KeepLastReliable.TransientLocal* and applies: + +- *RELIABLE* Reliability QoS and *TRANSIENT_LOCAL* Durability QoS, matching the QoS the Security Plugins use for their builtin secure-logging writers. +- *KEEP_LAST, depth=64* History QoS (with a matching `max_samples` resource limit), the default history depth used by the secure-logging writers. +- Dynamic memory allocation for the DataReader cache, since the secure-logging type is unbounded. + ## Application-specific QoS: [NonSecureAppsQos.xml](NonSecureAppsQos.xml) and [SecureAppsQos.xml](SecureAppsQos.xml) In addition to the [Qos.xml](#qosxml) file, this reference architecture describes *per-application* QoS in 2 additional XML files: diff --git a/system_arch/security/README.md b/system_arch/security/README.md index 3b1c93b..cb3e260 100644 --- a/system_arch/security/README.md +++ b/system_arch/security/README.md @@ -42,6 +42,7 @@ Certificates expiring within 30 days are flagged as warnings. Use `--warn-days N | --- | --- | | *(no flags)* | Generate artifacts (skip existing) | | `--force` | Re-generate all artifacts, overwriting existing ones | +| `--scaffold` | Re-generate the committed `.cnf`, governance, and permissions files from the Jinja2 templates (maintainer-only — see [Scaffolding](#scaffolding-maintainer-only)) | | `--strict` | Promote warnings to fatal errors | | `--status` | Report certificate expiry status and exit | | `--warn-days N` | Days-to-expiry warning threshold for `--status` (default: 30) | @@ -82,7 +83,7 @@ system_arch/security/ - **Chain files:** Identity certificates include a `.chain.pem` containing both the leaf cert and its issuing CA cert, as required by the RTI Security Plugins. - **Signed XML:** Governance and permissions XML files are S/MIME-signed by the appropriate intermediate CA. The signed `.p7s` files are what Connext loads at runtime. - **Per-participant permissions:** Each participant has its own permissions document specifying the exact topics it may publish/subscribe to, with a default `DENY` rule. -- **PSK passphrases:** Pre-Shared Key seed files (`.psk`) are generated per domain scope and stored alongside the governance/permissions artifacts (e.g. `domain_scope/TeleopWanDomain/TeleopWanDomain.psk`). The file format is `:` where `` is an integer in [0, 254] for Connext 7.3.x. Participants load the passphrase via the `dds.sec.crypto.rtps_psk_secret_passphrase` property. +- **PSK passphrases:** Pre-Shared Key seed files (`.psk`) are generated per domain scope and stored alongside the governance/permissions artifacts (e.g. `domain_scope/TeleopWanDomain/TeleopWanDomain.psk`). The file format is `:` where `` is an integer in [0, 254]. Participants load the passphrase via the `dds.sec.crypto.rtps_psk_secret_passphrase` property. ## Good Practices for DDS Security diff --git a/system_arch/security/domain_scope/OperationalDomain/permissions/SecureLogReader/SecureLogReader.xml b/system_arch/security/domain_scope/OperationalDomain/permissions/SecureLogReader/SecureLogReader.xml index 45f85ed..4a1e706 100644 --- a/system_arch/security/domain_scope/OperationalDomain/permissions/SecureLogReader/SecureLogReader.xml +++ b/system_arch/security/domain_scope/OperationalDomain/permissions/SecureLogReader/SecureLogReader.xml @@ -23,7 +23,13 @@ inability to use the software. --> 0 + + * + + + * + DDS:Security:LogTopicV2 diff --git a/system_arch/xml_app_creation/README.md b/system_arch/xml_app_creation/README.md index 7485eb0..09126b9 100644 --- a/system_arch/xml_app_creation/README.md +++ b/system_arch/xml_app_creation/README.md @@ -167,12 +167,13 @@ This Domain Library will hold all Domains defined as part of this reference arch | Domain | Domain ID | Intended Use | ------ | --------- | ------------ | [*OperationalDataDomain*](#connextdomainliboperationaldatadomain) | 0 | Real-time operational medical device data +| [*SecureLogDomain*](#connextdomainlibsecurelogdomain) | 0 | DDS Security builtin secure logging ##### ***ConnextDomainLib::OperationalDataDomain*** -In this reference architecture, you will find a single Domain defined, called *OperationalDataDomain* for Domain 0. +In this reference architecture, you will find two Domain definitions — *OperationalDataDomain* and *SecureLogDomain* — both configured for Domain ID `0`. Because they share the same Domain ID, they currently resolve to the **same** DDS domain. -*OperationalDataDomain* is named as such because as the system design scales over time, additional domains could be defined for monitoring, logging, etc. Those additional domains should not affect the performance of our operational data, and therefore should belong to a different domain. +*OperationalDataDomain* is named as such because as the system design scales over time, additional domains could be defined for monitoring, logging, etc. Those additional domains should not affect the performance of our operational data, and therefore should belong to a different domain. Keeping *SecureLogDomain* as a separate definition (see below) makes that future split straightforward: only its `domain_id` would need to change. *OperationalDataDomain* contains the following Topics (``): @@ -186,6 +187,16 @@ In this reference architecture, you will find a single Domain defined, called *O *Please refer to the data types referenced in the table above in [Types.xml](../Types.xml)* +##### ***ConnextDomainLib::SecureLogDomain*** + +*SecureLogDomain* holds the RTI Security Plugins builtin secure-logging Topic. It is consumed by the *dp/SecureLogReader* DomainParticipant. It is configured for Domain ID `0` (the same Domain as *OperationalDataDomain*) but defined separately so secure logging can later be isolated onto its own Domain ID without changing the operational configuration. + +*SecureLogDomain* contains the following Topic (``): + +| Topic | Data Type | Intended Use +| ----- | --------- | ------------ +| *DDS:Security:LogTopicV2* | *DDSSecurity::BuiltinLoggingTypeV2* | DDS Security builtin secure-logging stream + ### Domain Library Best Practices >**Best Practice:** Use prefixes to establish naming conventions and clearly indicate what is being named (e.g. "t/" prefix for Topics). This results in easier identification and readability. @@ -327,6 +338,7 @@ This reference architecture defines the following DomainParticipants in [Partici | [*dp/Orchestrator*](#medicaldemoparticipantlibrarydporchestrator) | [*ConnextDomainLib::OperationalDataDomain*](#connextdomainliboperationaldatadomain) | [*dp/PatientSensor*](#medicaldemoparticipantlibrarydppatientsensor) | [*ConnextDomainLib::OperationalDataDomain*](#connextdomainliboperationaldatadomain) | [*dp/PatientMonitor*](#medicaldemoparticipantlibrarydppatientmonitor) | [*ConnextDomainLib::OperationalDataDomain*](#connextdomainliboperationaldatadomain) +| [*dp/SecureLogReader*](#medicaldemoparticipantlibrarydpsecurelogreader) | [*ConnextDomainLib::SecureLogDomain*](#connextdomainlibsecurelogdomain) ##### ***MedicalDemoParticipantLibrary::dp/Arm*** @@ -417,6 +429,16 @@ The *PatientMonitor* DomainParticipant is intended to process (display) patient | *dr/DeviceCommand* | *t/DeviceCommand* | [*DataFlowLibrary::Command*](../qos/README.md#dataflowlibrarycommand-profile) | `device = 'PATIENT_MONITOR'` | Receive and process device commands targeting this PatientMonitor | *dr/Vitals* | *t/Vitals* | [*DataFlowLibrary::Streaming*](../qos/README.md#dataflowlibrarystreaming-profile) | -- | Receive and process patient vitals data stream +##### ***MedicalDemoParticipantLibrary::dp/SecureLogReader*** + +The *SecureLogReader* DomainParticipant subscribes to the RTI Security Plugins builtin secure-logging Topic, demonstrating how secure-log events can be consumed by an application. It operates on [*SecureLogDomain*](#connextdomainlibsecurelogdomain). + +*dp/SecureLogReader* contains the following DataReaders (``): + +| DataReader | Topic | QoS Profile | Content Filter | Intended Use +| ---------- | ----- | ----------- | -------------- | ------------ +| *dr/SecureLog* | *DDS:Security:LogTopicV2* | [*DataFlowLibrary::SecureLog*](../qos/README.md#dataflowlibrarysecurelog-profile) | -- | Receive DDS Security builtin secure-log samples + ### DomainParticipant Library Best Practices >**Best Practice:** Group your DataWriters into a single Publisher, and your DataReaders into a single Subscriber for a given DomainParticipant, unless your requirements dictate otherwise. diff --git a/tests/README.md b/tests/README.md index d52aa7c..746c6e5 100644 --- a/tests/README.md +++ b/tests/README.md @@ -142,4 +142,3 @@ pytest -m "gui" | `test_project_build_pipeline.py` | CMake configure & build; Module 01 C++ binaries & shared libraries exist | | `test_config_parsing.py` | Module JSON schemas and configuration parsing for all modules | | `test_security_status.py` | Security artifacts generation and availability for secure tests | -| `test_markdown_lint.py` | Project documentation (README, Scenario, etc.) passes rumdl checks | diff --git a/tests/docker/README.md b/tests/docker/README.md index 4161f16..cbec3e6 100644 --- a/tests/docker/README.md +++ b/tests/docker/README.md @@ -30,7 +30,7 @@ export RTI_LICENSE_FILE=/path/to/rti_license.dat ### Using Docker Compose (recommended) ```bash -# Run all tests (default: verbose output) +# Run all tests (default: the full suite over the repo) docker compose -f tests/docker/docker-compose.yml run --rm --build test # Run specific test file with custom pytest args @@ -59,7 +59,7 @@ docker compose -f tests/docker/docker-compose.yml down --remove-orphans docker build -f tests/docker/Dockerfile --target build -t medtech-build . docker build -f tests/docker/Dockerfile --target test -t medtech-test . -# 2. Run all tests (mounts license, default pytest with -v) +# 2. Run all tests (mounts license; runs the full suite) docker run --rm \ -v $RTI_LICENSE_FILE:/opt/rti.com/rti_license.dat:ro \ medtech-test @@ -91,7 +91,7 @@ The Docker entrypoint (`tests/docker/entrypoint.sh`): 3. Runs `pytest` with any provided arguments 4. Cleans up Xvfb on exit -Example: `docker run medtech-test` → entrypoint calls `pytest -v` +Example: `docker run medtech-test` → entrypoint calls `pytest .` (the full suite) Example: `docker run medtech-test -k "test_types"` → entrypoint calls `pytest -k "test_types"` From f25c7133336a49a2ab178dcdaaf60dda0c10d720 Mon Sep 17 00:00:00 2001 From: Jose Maria Lopez Vega Date: Wed, 24 Jun 2026 23:50:47 +0200 Subject: [PATCH 2/4] Added SecureSystemObserver snippet. Added ability to generate secure-apps qos with fully resolved absolute paths. Improved logging of setup_security scrip when skipping files. --- README.md | 5 + system_arch/qos/README.md | 45 ++++++- system_arch/qos/SecureExternalAppsQos.xml | 90 ++++++++++++++ system_arch/security/.gitignore | 5 + system_arch/security/README.md | 3 +- system_arch/security/dds_security.py | 12 +- .../SystemObserver/SystemObserver.xml | 44 +++++++ .../SystemObserver/SystemObserver.cnf | 17 +++ system_arch/security/security_tree.py | 76 +++++++++++- system_arch/security/setup_security.py | 113 +++++++++++++++++- 10 files changed, 393 insertions(+), 17 deletions(-) create mode 100644 system_arch/qos/SecureExternalAppsQos.xml create mode 100644 system_arch/security/domain_scope/OperationalDomain/permissions/SystemObserver/SystemObserver.xml create mode 100644 system_arch/security/identity/operating-room/SystemObserver/SystemObserver/SystemObserver.cnf diff --git a/README.md b/README.md index 5d16184..71c9ef8 100644 --- a/README.md +++ b/README.md @@ -310,6 +310,7 @@ This reference architecture defines the following DomainParticipants in [Partici | OperationalDataDomain | Orchestrator | `t/DeviceStatus`, `t/DeviceHeartbeat` | `t/DeviceCommand` | Administer device-level commands and monitor presence and status of all devices. | OperationalDataDomain | PatientSensor | `t/DeviceCommand` | `t/Vitals`, `t/DeviceStatus`, `t/DeviceHeartbeat` | Stream simulated patient vitals. | OperationalDataDomain | PatientMonitor | `t/DeviceCommand`, `t/Vitals` | `t/DeviceStatus`, `t/DeviceHeartbeat` | Process and display patient vitals. +| OperationalDataDomain | SystemObserver (external/optional) | All available topics | -- | Read-only observer integration: subscribes to every operational Topic and publishes nothing. Its DDS Security permissions allow subscribe on any Topic and deny all publication. | SecureLogDomain | SecureLogReader | `DDS:Security:LogTopicV2` | -- | Subscribe to the DDS Security builtin secure-log topic. *Note, this reference architecture utilizes one DomainParticipant for each device application. It is a **best practice** to define one DomainParticipant per application. However, in more complex systems, an application may be required to operate on multiple Domains. This requires defining multiple DomainParticipants for those applications that run in parallel.* @@ -328,6 +329,10 @@ The reference architecture configures security in [SecureAppsQos.xml](./system_a | **WAN Communications** | `TeleopWanDomain` governance for WAN connections (Module 03), including PSK-protected RTPS | **RTI Services** | Dedicated security profiles for Recording/Replay Services and Routing Services +For independent, security-specific observer integrations (not part of the demo applications), use [SecureExternalAppsQos.xml](./system_arch/qos/SecureExternalAppsQos.xml). It provides the `SecureExternalAppsQosLib::SecureSystemObserver` QoS snippet, which is intended to be composed into external DomainParticipants. + +This external snippet is ideal for Connext Studio when configuring an RTI Spy source: include [SecureExternalAppsQos.xml](./system_arch/qos/SecureExternalAppsQos.xml) and apply `SecureSystemObserver` so Spy can attach as a read-only secure observer. + Security Artifacts Structure in [security](./system_arch/security/): - `ca/` - Certificate Authority hierarchy (root CA → intermediate identity CA + intermediate permissions CA) diff --git a/system_arch/qos/README.md b/system_arch/qos/README.md index a016270..fad1963 100644 --- a/system_arch/qos/README.md +++ b/system_arch/qos/README.md @@ -18,6 +18,7 @@ This README describes how we've approached QoS in this reference architecture. F - [DataFlowLibrary::Heartbeat profile](#dataflowlibraryheartbeat-profile) - [DataFlowLibrary::SecureLog profile](#dataflowlibrarysecurelog-profile) - [Application-specific QoS: NonSecureAppsQos.xml and SecureAppsQos.xml](#application-specific-qos-nonsecureappsqosxml-and-secureappsqosxml) +- [External security snippets: SecureExternalAppsQos.xml](#external-security-snippets-secureexternalappsqosxml) - [XML QoS Best Practices](#xml-qos-best-practices) ## QoS Profile Configuration @@ -177,16 +178,56 @@ Both files contain only 1 QoS library: ***DpQosLib***. This QoS library contains [NonSecureAppsQos.xml](./NonSecureAppsQos.xml) contains one profile for each DomainParticipant. For the simplified demonstration, each profile inherits from *SystemLibrary::DefaultParticipant* in [Qos.xml](./Qos.xml). No additional configuration is applied for any given DomainParticipant. -[SecureAppsQos.xml](./SecureAppsQos.xml) also defines one profile for each DomainParticipant in a similar way to that of **NonSecureAppsQos.xml**, but with security configuration added. +[SecureAppsQos.xml](./SecureAppsQos.xml) defines secure profiles for the demo DomainParticipants and services in a similar way to **NonSecureAppsQos.xml**, but with security configuration added. [SecureAppsQos.xml](./SecureAppsQos.xml) defines a QoS snippet - *LanCommonSecurityConfig* defines common configuration to enable security for local domains (LAN connections). It references common permissions CA, identity CA, and governance files. [SecureAppsQos.xml](./SecureAppsQos.xml) defines a QoS snippet - *WanCommonSecurityConfig* defines common configuration to enable security for remote domains (WAN connections). It references common permissions CA, identity CA, and governance files. +## External security snippets: [SecureExternalAppsQos.xml](SecureExternalAppsQos.xml) + +[SecureExternalAppsQos.xml](./SecureExternalAppsQos.xml) provides standalone QoS snippets for participants that are **not** part of the demo applications themselves. Unlike the profiles in [SecureAppsQos.xml](./SecureAppsQos.xml), these are independent, Security-specific configurations meant to plug external observers into the secured system. + +The file currently defines one snippet: + +- **`SecureExternalAppsQosLib::SecureSystemObserver`** — a reusable [QoS Snippet](https://community.rti.com/best-practices/qos-profile-inheritance-and-composition-guidance#h.wr6u1ebybeff) that encapsulates the complete DDS Security property set for a read-only System Observer: + - Composes `BuiltinQosSnippetLib::Feature.Security.Enable` to activate the Security Plugins + - Permissions CA and Identity CA trust anchors + - Operational domain governance + - SystemObserver identity certificate and private key + - SystemObserver signed permissions document + - Secure logging disabled (`mode_mask=BUILTIN`, `verbosity=SILENT`) because the observer has no publish permissions + +### Usage + +Compose the snippet into any DomainParticipant QoS using the `` element: + +```xml + + + SecureExternalAppsQosLib::SecureSystemObserver + + +``` + +### Connext Studio (Spy source) + +This configuration is ideal for use with **RTI Connext Studio**. To observe the secured domain with the Spy data source: + +1. From the repository root, generate resolved QoS files with absolute security-artifact paths: + + ```bash + python3 system_arch/security/setup_security.py --generate-resolved-qos + ``` + +2. In Connext Studio, add a Spy Source and configure the source with the configuration under +`SecureExternalAppsQosLib::SecureSystemObserver` snippet from the `system_arch/security/resolved_qos/SecureExternalAppsQos.xml`. +3. Spy will join the secured Operational Domain as a read-only observer — able to subscribe to all Topics without publish permissions. + ## XML QoS Best Practices >**Best Practice:** Compose your QoS profiles with [QoS Snippets](https://community.rti.com/best-practices/qos-profile-inheritance-and-composition-guidance#h.wr6u1ebybeff). Snippets provide easier readability and result in more maintainable QoS for increasingly complex systems by reducing repetitive configuration. > >**Best Practice:** Inherit from [Built-in QoS Profiles](https://community.rti.com/static/documentation/connext-dds/7.7.0/doc/manuals/connext_dds_professional/users_manual/users_manual/Built_in_QoS_Profiles.htm). Builtin profiles provide starting points to frequently used and tuned QoS combinations. -Please take a look at the comments inside the profiles in [Qos.xml](./Qos.xml), [NonSecureAppsQos.xml](./NonSecureAppsQos.xml), and [SecureAppsQos.xml](./SecureAppsQos.xml) for further details on each QoS policy and more **best practices** related to QoS configuration. +Please take a look at the comments inside the profiles and snippets in [Qos.xml](./Qos.xml), [NonSecureAppsQos.xml](./NonSecureAppsQos.xml), [SecureAppsQos.xml](./SecureAppsQos.xml), and [SecureExternalAppsQos.xml](./SecureExternalAppsQos.xml) for further details on each QoS policy and more **best practices** related to QoS configuration. diff --git a/system_arch/qos/SecureExternalAppsQos.xml b/system_arch/qos/SecureExternalAppsQos.xml new file mode 100644 index 0000000..a8be59a --- /dev/null +++ b/system_arch/qos/SecureExternalAppsQos.xml @@ -0,0 +1,90 @@ + + + + + + + + + RTI_SECURITY_ARTIFACTS_DIR + ../../system_arch/security + + + + + + + + + + + BuiltinQosSnippetLib::Feature.Security.Enable + + + + + + dds.sec.access.permissions_ca + file:$(RTI_SECURITY_ARTIFACTS_DIR)/ca/TrustedPermissionsCa/certs/TrustedRootCa/TrustedPermissionsCa.crt + + + + dds.sec.auth.identity_ca + file:$(RTI_SECURITY_ARTIFACTS_DIR)/ca/TrustedIdentityCa/certs/TrustedRootCa/TrustedIdentityCa.crt + + + dds.sec.access.governance + file:$(RTI_SECURITY_ARTIFACTS_DIR)/domain_scope/OperationalDomain/governance/OperationalDomain/signed/TrustedPermissionsCa/OperationalDomain.p7s + + + dds.sec.auth.identity_certificate + file:$(RTI_SECURITY_ARTIFACTS_DIR)/identity/operating-room/SystemObserver/SystemObserver/certs/TrustedIdentityCa/SystemObserver.chain.pem + + + dds.sec.auth.private_key + file:$(RTI_SECURITY_ARTIFACTS_DIR)/identity/operating-room/SystemObserver/SystemObserver/private/SystemObserver.key + + + dds.sec.access.permissions + file:$(RTI_SECURITY_ARTIFACTS_DIR)/domain_scope/OperationalDomain/permissions/SystemObserver/signed/TrustedPermissionsCa/SystemObserver.p7s + + + + com.rti.serv.secure.logging.mode_mask + BUILTIN + + + com.rti.serv.secure.logging.verbosity + SILENT + + + + + + + + \ No newline at end of file diff --git a/system_arch/security/.gitignore b/system_arch/security/.gitignore index dc7eab2..88bb55a 100644 --- a/system_arch/security/.gitignore +++ b/system_arch/security/.gitignore @@ -31,3 +31,8 @@ identity/**/certs/ # Scaffold hash sidecar # --------------------------------------------------------------------------- .scaffold-hash + +# --------------------------------------------------------------------------- +# Resolved QoS files (generated by --generate-resolved-qos) +# --------------------------------------------------------------------------- +resolved_qos/ diff --git a/system_arch/security/README.md b/system_arch/security/README.md index cb3e260..f4dec87 100644 --- a/system_arch/security/README.md +++ b/system_arch/security/README.md @@ -47,6 +47,7 @@ Certificates expiring within 30 days are flagged as warnings. Use `--warn-days N | `--status` | Report certificate expiry status and exit | | `--warn-days N` | Days-to-expiry warning threshold for `--status` (default: 30) | | `--connext-version X.Y.Z` | Override auto-detected Connext version | +| `--generate-resolved-qos` | Generate resolved QoS XML files in `system_arch/security/resolved_qos/` with absolute security artifact paths; skips existing files unless `--force` is set | ## Directory Layout @@ -82,7 +83,7 @@ system_arch/security/ - **CA hierarchy:** A self-signed root CA (`TrustedRootCa`) issues two intermediate CAs — one for identity certificates (`TrustedIdentityCa`) and one for permissions/governance signing (`TrustedPermissionsCa`). - **Chain files:** Identity certificates include a `.chain.pem` containing both the leaf cert and its issuing CA cert, as required by the RTI Security Plugins. - **Signed XML:** Governance and permissions XML files are S/MIME-signed by the appropriate intermediate CA. The signed `.p7s` files are what Connext loads at runtime. -- **Per-participant permissions:** Each participant has its own permissions document specifying the exact topics it may publish/subscribe to, with a default `DENY` rule. +- **Per-participant permissions:** Each participant has its own permissions document specifying the exact topics it may publish/subscribe to, with a default `DENY` rule. For example, the `SystemObserver` participant grants `subscribe` on any topic and no `publish` rule at all — a least-privilege, read-only observer that can watch the full data flow but can never write to the bus. - **PSK passphrases:** Pre-Shared Key seed files (`.psk`) are generated per domain scope and stored alongside the governance/permissions artifacts (e.g. `domain_scope/TeleopWanDomain/TeleopWanDomain.psk`). The file format is `:` where `` is an integer in [0, 254]. Participants load the passphrase via the `dds.sec.crypto.rtps_psk_secret_passphrase` property. ## Good Practices for DDS Security diff --git a/system_arch/security/dds_security.py b/system_arch/security/dds_security.py index a3b464c..44c4140 100755 --- a/system_arch/security/dds_security.py +++ b/system_arch/security/dds_security.py @@ -438,7 +438,7 @@ def generate_root_ca( ) -> Path: """(b) Generate a root CA private key and self-signed certificate.""" if out_cert.is_file() and not force: - log.info("Root CA cert exists, skipping: %s", out_cert) + log.warning("Root CA cert already exists, skipping: %s — remove the file or use --force to regenerate", out_cert) return out_cert generate_key(key_path) return self_sign(key_path, cnf, out_cert, days=days) @@ -473,7 +473,7 @@ def generate_intermediate_ca( ) -> Path: """(d-f) Generate an intermediate CA: private key, CSR, and signed certificate.""" if out_cert.is_file() and not force: - log.info("Intermediate CA cert exists, skipping: %s", out_cert) + log.warning("Intermediate CA cert already exists, skipping: %s — remove the file or use --force to regenerate", out_cert) return out_cert generate_key(key_path) csr = out_cert.with_suffix(".csr") @@ -528,7 +528,7 @@ def generate_identity( intermediate (not directly the root). """ if out_cert.is_file() and not force: - log.info("Identity cert exists, skipping: %s", out_cert) + log.warning("Identity cert already exists, skipping: %s — remove the file or use --force to regenerate", out_cert) return out_cert generate_key(key_path) csr = out_cert.with_suffix(".csr") @@ -571,7 +571,7 @@ def generate_expired_identity( creation time. """ if out_cert.is_file() and not force: - log.info("Expired identity cert exists, skipping: %s", out_cert) + log.warning("Expired identity cert already exists, skipping: %s — remove the file or use --force to regenerate", out_cert) return out_cert generate_key(key_path) csr = out_cert.with_suffix(".csr") @@ -634,7 +634,7 @@ def sign_governance( ) -> Path: """(m) Sign a governance XML with S/MIME v3.2.""" if out_p7s.is_file() and not force: - log.info("Signed governance exists, skipping: %s", out_p7s) + log.warning("Signed governance already exists, skipping: %s — remove the file or use --force to regenerate", out_p7s) return out_p7s return sign_xml(key_path, cert_path, xml_path, out_p7s) @@ -654,7 +654,7 @@ def sign_permissions( ) -> Path: """(o) Sign a permissions XML with S/MIME v3.2.""" if out_p7s.is_file() and not force: - log.info("Signed permissions exists, skipping: %s", out_p7s) + log.warning("Signed permissions already exists, skipping: %s — remove the file or use --force to regenerate", out_p7s) return out_p7s return sign_xml(key_path, cert_path, xml_path, out_p7s) diff --git a/system_arch/security/domain_scope/OperationalDomain/permissions/SystemObserver/SystemObserver.xml b/system_arch/security/domain_scope/OperationalDomain/permissions/SystemObserver/SystemObserver.xml new file mode 100644 index 0000000..c02630b --- /dev/null +++ b/system_arch/security/domain_scope/OperationalDomain/permissions/SystemObserver/SystemObserver.xml @@ -0,0 +1,44 @@ + + + + + + /C=US/ST=CA/O=Company Name/emailAddress=systemobserver@company_name.com/CN=SystemObserver + + + 2024-06-01T13:00:00 + 2037-06-01T13:00:00 + + + + + 0 + + + * + + + + * + + + * + + + + DENY + + + diff --git a/system_arch/security/identity/operating-room/SystemObserver/SystemObserver/SystemObserver.cnf b/system_arch/security/identity/operating-room/SystemObserver/SystemObserver/SystemObserver.cnf new file mode 100644 index 0000000..5989dce --- /dev/null +++ b/system_arch/security/identity/operating-room/SystemObserver/SystemObserver/SystemObserver.cnf @@ -0,0 +1,17 @@ +[ req ] +prompt = no +distinguished_name = req_distinguished_name + +[ req_distinguished_name ] +countryName = US +stateOrProvinceName = CA +organizationName = Company Name +emailAddress = systemobserver@company_name.com +commonName = SystemObserver + +[ usr_cert ] +# End-entity certificate extensions +basicConstraints = critical, CA:false +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer +keyUsage = critical, digitalSignature, keyEncipherment diff --git a/system_arch/security/security_tree.py b/system_arch/security/security_tree.py index ee24927..773fdc2 100644 --- a/system_arch/security/security_tree.py +++ b/system_arch/security/security_tree.py @@ -287,20 +287,39 @@ def _identity_dir(self, root: Path, module_name: str, app_name: str, id_name: st # -- Artifact generation ------------------------------------------------ - def generate_artifacts(self, root: Path, force: bool = False, strict: bool = False) -> None: + def generate_artifacts( + self, root: Path, force: bool = False, strict: bool = False + ) -> dict[str, int]: """Generate all keys, certificates, and signed XML files. Args: root: Root directory for artifact output. force: Re-generate even if artifacts already exist. strict: Promote warnings to fatal errors. + + Returns: + Summary counters for generated and skipped artifacts. """ warnings: list[str] = [] + summary: dict[str, int] = { + "warnings": 0, + "ca_certs_generated": 0, + "ca_certs_skipped": 0, + "signed_governance_generated": 0, + "signed_governance_skipped": 0, + "signed_permissions_generated": 0, + "signed_permissions_skipped": 0, + "psk_seeds_generated": 0, + "psk_seeds_skipped": 0, + "identity_certs_generated": 0, + "identity_certs_skipped": 0, + } def _warn(msg: str) -> None: """Log a warning; in strict mode, collect for later abort.""" log.warning(msg) + summary["warnings"] += 1 if strict: warnings.append(msg) @@ -436,20 +455,30 @@ def _resolve_ca(ca_def: CA) -> Path: _check_key_perms(_ca_key(ca_def)) if ca_def.self_signed: + out_cert = _ca_cert(ca_def) + if out_cert.is_file() and not force: + summary["ca_certs_skipped"] += 1 + else: + summary["ca_certs_generated"] += 1 cert = generate_root_ca( _ca_key(ca_def), _ca_cnf(ca_def), - _ca_cert(ca_def), + out_cert, force=force, ) _check_cert_validity(cert, f"Root CA '{ca_def.name}'") else: issuer_cert = _resolve_ca(ca_def.issuer) issuer_dir = self._ca_dir(root, ca_def.issuer) + out_cert = _ca_cert(ca_def, ca_def.issuer) + if out_cert.is_file() and not force: + summary["ca_certs_skipped"] += 1 + else: + summary["ca_certs_generated"] += 1 cert = generate_intermediate_ca( _ca_key(ca_def), _ca_cnf(ca_def), - _ca_cert(ca_def, ca_def.issuer), + out_cert, issuer_cnf=_ca_cnf(ca_def.issuer), issuer_key=_ca_key(ca_def.issuer), issuer_cert=issuer_cert, @@ -500,13 +529,19 @@ def _resolve_ca(ca_def: CA) -> Path: if not gov_xml.is_file(): _warn(f"Governance XML not found: {gov_xml} — did you run --scaffold first?") + gov_p7s = gov_dir / "signed" / gov.issuer.name / f"{gov.name}.p7s" + gov_p7s_preexisting = gov_p7s.is_file() sign_governance( perm_ca_key, perm_ca_cert, gov_xml, - gov_dir / "signed" / gov.issuer.name / f"{gov.name}.p7s", + gov_p7s, force=force, ) + if gov_p7s_preexisting and not force: + summary["signed_governance_skipped"] += 1 + else: + summary["signed_governance_generated"] += 1 for perm in scope.permissions: p_ca_cert = _resolve_ca(perm.issuer) @@ -518,13 +553,19 @@ def _resolve_ca(ca_def: CA) -> Path: if not perm_xml.is_file(): _warn(f"Permissions XML not found: {perm_xml} — did you run --scaffold first?") + perm_p7s = perm_dir / "signed" / perm.issuer.name / f"{perm.name}.p7s" + perm_p7s_preexisting = perm_p7s.is_file() sign_permissions( p_ca_key, p_ca_cert, perm_xml, - perm_dir / "signed" / perm.issuer.name / f"{perm.name}.p7s", + perm_p7s, force=force, ) + if perm_p7s_preexisting and not force: + summary["signed_permissions_skipped"] += 1 + else: + summary["signed_permissions_generated"] += 1 # PSK seed files (per domain scope) for scope in self.domain_scopes: @@ -536,8 +577,10 @@ def _resolve_ca(ca_def: CA) -> Path: seed = generate_psk_seed(psk.length) psk_file.write_text(f"{psk.id}:{seed}") log.info("Generated PSK seed file: %s", psk_file) + summary["psk_seeds_generated"] += 1 else: - log.info("PSK seed file exists, skipping: %s", psk_file) + log.warning("PSK seed file already exists, skipping: %s — remove the file or use --force to regenerate", psk_file) + summary["psk_seeds_skipped"] += 1 # Identities for module in self.modules: @@ -559,6 +602,10 @@ def _resolve_ca(ca_def: CA) -> Path: _check_key_perms(id_key) + if id_cert.is_file() and not force: + summary["identity_certs_skipped"] += 1 + else: + summary["identity_certs_generated"] += 1 generate_identity( id_key, id_cnf, @@ -598,6 +645,23 @@ def _resolve_ca(ca_def: CA) -> Path: _check_strict() + summary["total_generated"] = ( + summary["ca_certs_generated"] + + summary["signed_governance_generated"] + + summary["signed_permissions_generated"] + + summary["psk_seeds_generated"] + + summary["identity_certs_generated"] + ) + summary["total_skipped"] = ( + summary["ca_certs_skipped"] + + summary["signed_governance_skipped"] + + summary["signed_permissions_skipped"] + + summary["psk_seeds_skipped"] + + summary["identity_certs_skipped"] + ) + + return summary + # -- Validation helpers ------------------------------------------------- def _validate_subject_name(self, root: Path, identity, module, app, warn_fn) -> None: diff --git a/system_arch/security/setup_security.py b/system_arch/security/setup_security.py index 583f0ef..84afb97 100755 --- a/system_arch/security/setup_security.py +++ b/system_arch/security/setup_security.py @@ -15,11 +15,15 @@ # committing them to the repo. python3 setup_security.py --scaffold + # Generate QoS XML files with fully resolved absolute paths + python3 setup_security.py --generate-resolved-qos + Prerequisite: ``NDDSHOME`` must be set to the Connext installation path. """ import argparse import logging +import re import subprocess from pathlib import Path @@ -116,6 +120,16 @@ publish_topics=[], subscribe_topics=["DDS:Security:LogTopicV2"], ), + # Read-only system observer: may subscribe to any topic, may publish + # to none. The committed SystemObserver.xml is hand-edited to grant + # subscribe on all topics/partitions (*), mirroring the + # SecureLogReader pattern; publish is intentionally empty. + Permissions( + name="SystemObserver", + issuer=TRUSTED_PERMISSIONS_CA, + publish_topics=[], + subscribe_topics=["*"], + ), Permissions( name="Test", issuer=TRUSTED_PERMISSIONS_CA, @@ -179,6 +193,10 @@ name="PatientSensor", identities=[Identity(name="PatientSensor", issuer=TRUSTED_IDENTITY_CA)], ), + App( + name="SystemObserver", + identities=[Identity(name="SystemObserver", issuer=TRUSTED_IDENTITY_CA)], + ), App(name="Test", identities=[Identity(name="Test", issuer=TRUSTED_IDENTITY_CA)]), ], ) @@ -242,6 +260,72 @@ ) +QOS_DIR = SECURITY_DIR.parent / "qos" + +# QoS XML files to process when --generate-resolved-qos is used +_QOS_FILES_TO_RESOLVE = [ + "SecureAppsQos.xml", + "SecureExternalAppsQos.xml", +] + +# Regex matching the block that defines +# RTI_SECURITY_ARTIFACTS_DIR (including surrounding whitespace). +_CONFIG_VARS_RE = re.compile( + r"\n\s*.*?\n", + re.DOTALL, +) + + +def generate_resolved_qos(security_dir: Path, force: bool = False) -> None: + """Generate QoS XML files with $(RTI_SECURITY_ARTIFACTS_DIR) resolved to absolute paths. + + Reads each source file from the qos/ directory, replaces the variable + reference with the absolute security directory path, removes the + block (no longer needed), and writes the + result to /resolved_qos/. + + Existing files are skipped unless ``force`` is True. + """ + log = logging.getLogger(__name__) + out_dir = security_dir / "resolved_qos" + out_dir.mkdir(parents=True, exist_ok=True) + + abs_security_path = str(security_dir) + written_count = 0 + skipped_count = 0 + + for filename in _QOS_FILES_TO_RESOLVE: + src = QOS_DIR / filename + if not src.is_file(): + log.warning("QoS source file not found, skipping: %s", src) + continue + + content = src.read_text() + + # Replace the variable reference with the absolute path + content = content.replace("$(RTI_SECURITY_ARTIFACTS_DIR)", abs_security_path) + + # Remove the block since paths are now absolute + content = _CONFIG_VARS_RE.sub("\n", content) + + dest = out_dir / filename + if dest.exists() and not force: + log.warning( + "Resolved QoS file already exists, skipping: %s - remove the file or use --force to regenerate", + dest, + ) + skipped_count += 1 + continue + + dest.write_text(content) + log.info("Resolved QoS file written: %s", dest) + written_count += 1 + + print( + f"Resolved QoS generation complete: {written_count} written, {skipped_count} skipped; output directory: {out_dir}" + ) + + def main(): parser = argparse.ArgumentParser( description="Generate DDS Security artifacts for the reference architecture." @@ -280,6 +364,14 @@ def main(): help="Override Connext version (e.g. '7.5.0'). " "Auto-detected from rti.connextdds if not set.", ) + parser.add_argument( + "--generate-resolved-qos", + action="store_true", + help="Generate QoS XML files (SecureAppsQos.xml, SecureExternalAppsQos.xml) " + "with $(RTI_SECURITY_ARTIFACTS_DIR) resolved to absolute paths. " + "Output is written to a 'resolved_qos' subfolder under the security directory. " + "Existing files are skipped unless --force is set.", + ) args = parser.parse_args() level = (logging.WARNING, logging.INFO, logging.DEBUG)[min(args.verbose, 2)] @@ -300,9 +392,26 @@ def main(): elif args.scaffold: scaffold_tree(SECURITY_TREE, root=SECURITY_DIR, strict=args.strict) print(f"Security directory tree scaffolded under {SECURITY_DIR}") + elif args.generate_resolved_qos: + generate_resolved_qos(SECURITY_DIR, force=args.force) else: - SECURITY_TREE.generate_artifacts(root=SECURITY_DIR, force=args.force, strict=args.strict) - print("Security artifacts generated!") + summary = SECURITY_TREE.generate_artifacts( + root=SECURITY_DIR, force=args.force, strict=args.strict + ) + print( + "Security artifact generation complete: " + f"{summary['total_generated']} generated, " + f"{summary['total_skipped']} skipped, " + f"{summary['warnings']} validation warning(s)." + ) + print( + "Breakdown: " + f"CA certs {summary['ca_certs_generated']} generated/{summary['ca_certs_skipped']} skipped; " + f"signed governance {summary['signed_governance_generated']}/{summary['signed_governance_skipped']}; " + f"signed permissions {summary['signed_permissions_generated']}/{summary['signed_permissions_skipped']}; " + f"identity certs {summary['identity_certs_generated']}/{summary['identity_certs_skipped']}; " + f"PSK seeds {summary['psk_seeds_generated']}/{summary['psk_seeds_skipped']}." + ) if __name__ == "__main__": From fcd82fc631842d71887ac31be47d84859e50c675 Mon Sep 17 00:00:00 2001 From: Jose Maria Lopez Vega Date: Fri, 26 Jun 2026 16:36:27 +0200 Subject: [PATCH 3/4] Updated security configuration to match best practices. --- README.md | 11 ++-- modules/03-remote-teleoperation/README.md | 2 +- modules/04-security-threat/README.md | 10 +++- system_arch/qos/README.md | 4 +- system_arch/qos/SecureAppsQos.xml | 4 ++ system_arch/qos/SecureExternalAppsQos.xml | 28 ++++++---- system_arch/security/README.md | 5 +- .../OperationalDomain/OperationalDomain.xml | 21 +++++++- .../TeleopWanDomain/TeleopWanDomain.xml | 25 ++++----- system_arch/security/security_tree.py | 8 +-- system_arch/security/setup_security.py | 52 ++++++++++++++++++- .../security/templates/governance.xml.j2 | 1 + 12 files changed, 129 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 71c9ef8..9eceb28 100644 --- a/README.md +++ b/README.md @@ -321,12 +321,13 @@ DDS Security defines authentication, access control, and encryption capabilities DDS Security is meant to be a pluggable component to the system architecture. This reference architecture demonstrates the flexibility of the RTI Security Plugins, and how a system can be secured purely through configuration. It should be noted that enabling security does have an effect on performance - both at initialization due to authentication and in steady-state operation due to encryption. It is because of this, that a system's architecture should be designed with security in mind, even if application code has no dependency on the use of security. -The reference architecture configures security in [SecureAppsQos.xml](./system_arch/qos/SecureAppsQos.xml) with: +The reference architecture configures security in [SecureAppsQos.xml](./system_arch/qos/SecureAppsQos.xml) following the [Builtin Security Plugins domain-level protection](https://community.rti.com/static/documentation/connext-dds/current/doc/manuals/connext_dds_secure/users_manual/p3_advanced/threat_modeling.html#dds-security-threat-protection) pattern with topic-level protection for sensitive topics: | Component | Security Features | ---------------------- | ----------------- -| **LAN Communications** | `OperationalDomain` governance, participant-specific certificates and permissions -| **WAN Communications** | `TeleopWanDomain` governance for WAN connections (Module 03), including PSK-protected RTPS +| **LAN Communications** | `OperationalDomain` governance with `ENCRYPT_WITH_ORIGIN_AUTHENTICATION` RTPS protection, PSK encryption (`OperationalDomain.psk`), participant-specific certificates and permissions +| **WAN Communications** | `TeleopWanDomain` governance with `ENCRYPT_WITH_ORIGIN_AUTHENTICATION` RTPS protection, PSK encryption (`TeleopWanDomain.psk`) for WAN connections (Module 03) +| **Topic-level protection** | `t/Vitals` and `t/MotorControl` topics use `metadata_protection_kind=ENCRYPT` for insider confidentiality protection | **RTI Services** | Dedicated security profiles for Recording/Replay Services and Routing Services For independent, security-specific observer integrations (not part of the demo applications), use [SecureExternalAppsQos.xml](./system_arch/qos/SecureExternalAppsQos.xml). It provides the `SecureExternalAppsQosLib::SecureSystemObserver` QoS snippet, which is intended to be composed into external DomainParticipants. @@ -352,12 +353,12 @@ Check out the the [system_arch](./system_arch/) folder, where the system archite - [RTI XML-Based Application Creation](https://community.rti.com/static/documentation/connext-dds/7.7.0/doc/manuals/connext_dds_professional/xml_application_creation/xml_based_app_creation_guide/XMLAppCreationGSG_title.htm#) - [RTI System Designer](https://community.rti.com/static/documentation/connext-dds/7.7.0/doc/manuals/connext_dds_professional/tools/system_designer/index.html) - [RTI Core Libraries Users Manual](https://community.rti.com/static/documentation/connext-dds/7.7.0/doc/manuals/connext_dds_professional/users_manual/users_manual/title.htm#) -- [RTI Security Plugins](https://community.rti.com/static/documentation/connext-dds/7.7.0/doc/manuals/connext_dds_secure/users_manual/index.html) +- [RTI Security Plugins](https://community.rti.com/static/documentation/connext-dds/current/doc/manuals/connext_dds_secure/users_manual/index.html) - [RTI Connext Modern C++ API](https://community.rti.com/static/documentation/connext-dds/7.7.0/doc/api/connext_dds/api_cpp2/index.html) *, used in Module 01: Digital Operating Room* - [RTI Connext Python API](https://community.rti.com/static/documentation/connext-dds/7.7.0/doc/api/connext_dds/api_python/index.html) *, used in Module 01: Digital Operating Room* - [RTI Recording Service & Replay Service](https://community.rti.com/static/documentation/connext-dds/7.7.0/doc/manuals/connext_dds_professional/services/recording_service/introduction.html) *, used in Module 02: RTI Recording Service & RTI Replay Service* - [Connext Real-Time WAN Transport](https://community.rti.com/static/documentation/connext-dds/7.7.0/doc/manuals/connext_dds_professional/users_manual/users_manual/PartRealtimeWAN.htm) *, used in Module 03: Remote Teleoperation with RTI Real-Time WAN Transport* - [RTI Routing Service](https://community.rti.com/static/documentation/connext-dds/7.7.0/doc/manuals/connext_dds_professional/services/routing_service/index.html) *, used in Module 03: Remote Teleoperation with RTI Real-Time WAN Transport* - [RTI Cloud Discovery Service](https://community.rti.com/static/documentation/connext-dds/7.7.0/doc/manuals/addon_products/cloud_discovery_service/index.html) *, used in Module 03: Remote Teleoperation with RTI Real-Time WAN Transport* -- [RTI Security Plugins Users Manual](https://community.rti.com/static/documentation/connext-dds/7.7.0/doc/manuals/connext_dds_secure/users_manual/index.html) *, used in Module 04: Security Threat Demonstration* +- [RTI Security Plugins Users Manual](https://community.rti.com/static/documentation/connext-dds/current/doc/manuals/connext_dds_secure/users_manual/index.html) *, used in Module 04: Security Threat Demonstration* - [RTI Connext Third-Party Software](https://community.rti.com/static/documentation/connext-dds/7.7.0/doc/manuals/connext_dds_professional/release_notes_3rdparty/index.html) diff --git a/modules/03-remote-teleoperation/README.md b/modules/03-remote-teleoperation/README.md index 784d70e..b75bd20 100644 --- a/modules/03-remote-teleoperation/README.md +++ b/modules/03-remote-teleoperation/README.md @@ -37,7 +37,7 @@ Together, the RTI Real-Time WAN Transport, RTI Security Plugins, and RTI Cloud D - **Low-latency communication** across WAN connections - **Automatic NAT traversal** capabilities -- **Secure data transmission** with built-in authentication, encryption and access control +- **Secure data transmission** with domain-level protection (`ENCRYPT_WITH_ORIGIN_AUTHENTICATION` + PSK encryption) and topic-level encryption for sensitive topics (`t/Vitals`, `t/MotorControl`) - **Bandwidth optimization** for efficient data transfer (when compared to TCP-based communication) - **Connection resilience** with automatic reconnection diff --git a/modules/04-security-threat/README.md b/modules/04-security-threat/README.md index be8d40b..66d5c71 100644 --- a/modules/04-security-threat/README.md +++ b/modules/04-security-threat/README.md @@ -160,7 +160,7 @@ Watch the threat app's Activity Log: when the secured OR comes up, the injector' ### 2. Understanding Why Each Attack Is Blocked -Each attack mode corresponds to a different stage of the DDS Security handshake: +Each attack mode corresponds to a different stage of the DDS Security handshake. Even if an attacker passes one layer, subsequent layers still block the attack: | Mode | What happens | Why | | --- | --- | --- | @@ -168,7 +168,13 @@ Each attack mode corresponds to a different stage of the DDS Security handshake: | **Forged Permissions** | Participant is created but never matches | Authentication succeeds (identity signed by trusted CA), but the permissions document is signed by the rogue CA. Since the OR's `permissions_ca` is the trusted CA, the signature mismatch causes access control validation to fail. | | **Expired Certificate** | Participant creation fails immediately | The identity certificate was signed by the trusted CA but its `notAfter` field is in the past. The DDS Security authentication plugin validates certificate expiration during identity validation — for the local participant, this occurs during DomainParticipant creation, causing it to fail immediately. The status badge shows **ATTACK FAILED** (red). | -For a deeper dive into the DDS Security handshake, refer to the [RTI Security Plugins User's Manual](https://community.rti.com/static/documentation/connext-dds/7.7.0/doc/manuals/connext_dds_secure/users_manual/index.htm). +Beyond authentication and permissions, the system also enforces **cryptographic protection at multiple levels**: + +- **Domain-level protection from outsiders:** `rtps_psk_protection_kind=ENCRYPT` protects pre-authentication traffic, preventing passive eavesdropping before the handshake completes. +- **Domain-level protection from insiders:** `rtps_protection_kind=ENCRYPT_WITH_ORIGIN_AUTHENTICATION` ensures all RTPS traffic is encrypted with per-writer keys and origin-authenticated — even an authenticated insider cannot forge another participant's messages. +- **Topic-level protetion from insiders:** `t/Vitals` and `t/MotorControl` use `metadata_protection_kind=ENCRYPT`, meaning their submessage metadata is encrypted with keys shared only among authorized endpoints — a compromised participant without topic-level permissions cannot decrypt these topics. + +For a deeper dive into the DDS Security handshake, refer to the [RTI Security Plugins User's Manual](https://community.rti.com/static/documentation/connext-dds/current/doc/manuals/connext_dds_secure/users_manual/p2_core/authentication.html#handshake). --- diff --git a/system_arch/qos/README.md b/system_arch/qos/README.md index fad1963..0008103 100644 --- a/system_arch/qos/README.md +++ b/system_arch/qos/README.md @@ -180,9 +180,9 @@ Both files contain only 1 QoS library: ***DpQosLib***. This QoS library contains [SecureAppsQos.xml](./SecureAppsQos.xml) defines secure profiles for the demo DomainParticipants and services in a similar way to **NonSecureAppsQos.xml**, but with security configuration added. -[SecureAppsQos.xml](./SecureAppsQos.xml) defines a QoS snippet - *LanCommonSecurityConfig* defines common configuration to enable security for local domains (LAN connections). It references common permissions CA, identity CA, and governance files. +[SecureAppsQos.xml](./SecureAppsQos.xml) defines a QoS snippet - *LanCommonSecurityConfig* defines common configuration to enable security for local domains (LAN connections). It references common permissions CA, identity CA, governance files, and the OperationalDomain PSK seed file. The governance uses `rtps_protection_kind=ENCRYPT_WITH_ORIGIN_AUTHENTICATION` with `rtps_psk_protection_kind=ENCRYPT` for domain-level protection, plus topic-level protection (`metadata_protection_kind=ENCRYPT`) on `t/Vitals` and `t/MotorControl`. -[SecureAppsQos.xml](./SecureAppsQos.xml) defines a QoS snippet - *WanCommonSecurityConfig* defines common configuration to enable security for remote domains (WAN connections). It references common permissions CA, identity CA, and governance files. +[SecureAppsQos.xml](./SecureAppsQos.xml) defines a QoS snippet - *WanCommonSecurityConfig* defines common configuration to enable security for remote domains (WAN connections). It references common permissions CA, identity CA, governance files, and the TeleopWanDomain PSK seed file. The same domain-level protection applies (`rtps_protection_kind=ENCRYPT_WITH_ORIGIN_AUTHENTICATION`, `rtps_psk_protection_kind=ENCRYPT`), but the WAN governance applies *stricter* topic-level protection: a catch-all `*` rule with `metadata_protection_kind=ENCRYPT` protects the submessage metadata of **every** topic, rather than only `t/Vitals` and `t/MotorControl`. The secure log topic `DDS:Security:LogTopicV2` keeps its own `SIGN`/`ENCRYPT`. ## External security snippets: [SecureExternalAppsQos.xml](SecureExternalAppsQos.xml) diff --git a/system_arch/qos/SecureAppsQos.xml b/system_arch/qos/SecureAppsQos.xml index b75d33f..035787b 100644 --- a/system_arch/qos/SecureAppsQos.xml +++ b/system_arch/qos/SecureAppsQos.xml @@ -87,6 +87,10 @@ inability to use the software. --> dds.sec.access.governance file:$(RTI_SECURITY_ARTIFACTS_DIR)/domain_scope/OperationalDomain/governance/OperationalDomain/signed/TrustedPermissionsCa/OperationalDomain.p7s + + dds.sec.crypto.rtps_psk_secret_passphrase + file:$(RTI_SECURITY_ARTIFACTS_DIR)/domain_scope/OperationalDomain/OperationalDomain.psk + diff --git a/system_arch/qos/SecureExternalAppsQos.xml b/system_arch/qos/SecureExternalAppsQos.xml index a8be59a..565451f 100644 --- a/system_arch/qos/SecureExternalAppsQos.xml +++ b/system_arch/qos/SecureExternalAppsQos.xml @@ -43,22 +43,12 @@ inability to use the software. --> - - - dds.sec.access.permissions_ca - file:$(RTI_SECURITY_ARTIFACTS_DIR)/ca/TrustedPermissionsCa/certs/TrustedRootCa/TrustedPermissionsCa.crt - dds.sec.auth.identity_ca file:$(RTI_SECURITY_ARTIFACTS_DIR)/ca/TrustedIdentityCa/certs/TrustedRootCa/TrustedIdentityCa.crt - - dds.sec.access.governance - file:$(RTI_SECURITY_ARTIFACTS_DIR)/domain_scope/OperationalDomain/governance/OperationalDomain/signed/TrustedPermissionsCa/OperationalDomain.p7s - dds.sec.auth.identity_certificate file:$(RTI_SECURITY_ARTIFACTS_DIR)/identity/operating-room/SystemObserver/SystemObserver/certs/TrustedIdentityCa/SystemObserver.chain.pem @@ -67,10 +57,28 @@ inability to use the software. --> dds.sec.auth.private_key file:$(RTI_SECURITY_ARTIFACTS_DIR)/identity/operating-room/SystemObserver/SystemObserver/private/SystemObserver.key + + + + dds.sec.access.permissions_ca + file:$(RTI_SECURITY_ARTIFACTS_DIR)/ca/TrustedPermissionsCa/certs/TrustedRootCa/TrustedPermissionsCa.crt + + + dds.sec.access.governance + file:$(RTI_SECURITY_ARTIFACTS_DIR)/domain_scope/OperationalDomain/governance/OperationalDomain/signed/TrustedPermissionsCa/OperationalDomain.p7s + dds.sec.access.permissions file:$(RTI_SECURITY_ARTIFACTS_DIR)/domain_scope/OperationalDomain/permissions/SystemObserver/signed/TrustedPermissionsCa/SystemObserver.p7s + + + + dds.sec.crypto.rtps_psk_secret_passphrase + file:$(RTI_SECURITY_ARTIFACTS_DIR)/domain_scope/OperationalDomain/OperationalDomain.psk + + diff --git a/system_arch/security/README.md b/system_arch/security/README.md index f4dec87..0954777 100644 --- a/system_arch/security/README.md +++ b/system_arch/security/README.md @@ -59,6 +59,7 @@ system_arch/security/ │ └── TrustedPermissionsCa/ # Intermediate CA for permissions signing ├── domain_scope/ # Per-domain governance & permissions │ ├── OperationalDomain/ +│ │ ├── OperationalDomain.psk # PSK passphrase seed (generated) │ │ ├── governance//.xml # Governance XML (committed) │ │ │ └── signed// # Signed governance (.p7s) │ │ └── permissions//.xml # Permissions XML (committed) @@ -84,7 +85,9 @@ system_arch/security/ - **Chain files:** Identity certificates include a `.chain.pem` containing both the leaf cert and its issuing CA cert, as required by the RTI Security Plugins. - **Signed XML:** Governance and permissions XML files are S/MIME-signed by the appropriate intermediate CA. The signed `.p7s` files are what Connext loads at runtime. - **Per-participant permissions:** Each participant has its own permissions document specifying the exact topics it may publish/subscribe to, with a default `DENY` rule. For example, the `SystemObserver` participant grants `subscribe` on any topic and no `publish` rule at all — a least-privilege, read-only observer that can watch the full data flow but can never write to the bus. -- **PSK passphrases:** Pre-Shared Key seed files (`.psk`) are generated per domain scope and stored alongside the governance/permissions artifacts (e.g. `domain_scope/TeleopWanDomain/TeleopWanDomain.psk`). The file format is `:` where `` is an integer in [0, 254]. Participants load the passphrase via the `dds.sec.crypto.rtps_psk_secret_passphrase` property. +- **PSK passphrases:** Pre-Shared Key seed files (`.psk`) are generated per domain scope and stored alongside the governance/permissions artifacts (e.g. `domain_scope/OperationalDomain/OperationalDomain.psk`, `domain_scope/TeleopWanDomain/TeleopWanDomain.psk`). The file format is `:` where `` is an integer in [0, 254]. Participants load the passphrase via the `dds.sec.crypto.rtps_psk_secret_passphrase` property. Both domains use `rtps_psk_protection_kind=ENCRYPT` in their governance to protect pre-authentication RTPS traffic. +- **Domain-level protection:** Both `OperationalDomain` and `TeleopWanDomain` use the [Builtin Security Plugins for domain-level protection](https://community.rti.com/static/documentation/connext-dds/current/doc/manuals/connext_dds_secure/users_manual/p3_advanced/threat_modeling.html#dds-security-threat-protection) pattern: `rtps_protection_kind=ENCRYPT_WITH_ORIGIN_AUTHENTICATION` provides insider integrity and availability protection; `rtps_psk_protection_kind=ENCRYPT` secures pre-authentication traffic. +- **Topic-level protection:** In the `OperationalDomain` (LAN) governance, the `t/Vitals` and `t/MotorControl` topics use `metadata_protection_kind=ENCRYPT` for [topic-level protection](https://community.rti.com/static/documentation/connext-dds/current/doc/manuals/connext_dds_secure/users_manual/p3_advanced/threat_modeling.html#dds-security-threat-protection), ensuring only participants with matching permissions can decrypt those topics' submessage metadata — even if authenticated to the domain. The `TeleopWanDomain` (WAN) governance is stricter: a catch-all `*` rule applies `metadata_protection_kind=ENCRYPT` to **every** topic, with the `DDS:Security:LogTopicV2` rule ordered ahead of it so the secure log retains its own `SIGN`/`ENCRYPT` protection. ## Good Practices for DDS Security diff --git a/system_arch/security/domain_scope/OperationalDomain/governance/OperationalDomain/OperationalDomain.xml b/system_arch/security/domain_scope/OperationalDomain/governance/OperationalDomain/OperationalDomain.xml index 0ea17f8..8591132 100644 --- a/system_arch/security/domain_scope/OperationalDomain/governance/OperationalDomain/OperationalDomain.xml +++ b/system_arch/security/domain_scope/OperationalDomain/governance/OperationalDomain/OperationalDomain.xml @@ -22,8 +22,27 @@ inability to use the software. --> true NONE NONE - ENCRYPT + ENCRYPT_WITH_ORIGIN_AUTHENTICATION + ENCRYPT + + t/Vitals + false + false + true + true + ENCRYPT + NONE + + + t/MotorControl + false + false + true + true + ENCRYPT + NONE + * false diff --git a/system_arch/security/domain_scope/TeleopWanDomain/governance/TeleopWanDomain/TeleopWanDomain.xml b/system_arch/security/domain_scope/TeleopWanDomain/governance/TeleopWanDomain/TeleopWanDomain.xml index 06e6dd6..08cddcb 100644 --- a/system_arch/security/domain_scope/TeleopWanDomain/governance/TeleopWanDomain/TeleopWanDomain.xml +++ b/system_arch/security/domain_scope/TeleopWanDomain/governance/TeleopWanDomain/TeleopWanDomain.xml @@ -22,33 +22,26 @@ inability to use the software. --> true NONE NONE - - ENCRYPT + ENCRYPT_WITH_ORIGIN_AUTHENTICATION ENCRYPT - * + DDS:Security:LogTopicV2 false false true - true - NONE - NONE + false + SIGN + ENCRYPT - DDS:Security:LogTopicV2 + * false false true - false - SIGN - ENCRYPT + true + ENCRYPT + NONE diff --git a/system_arch/security/security_tree.py b/system_arch/security/security_tree.py index 773fdc2..b09d4e7 100644 --- a/system_arch/security/security_tree.py +++ b/system_arch/security/security_tree.py @@ -96,9 +96,10 @@ class Governance: domain_id_max: Optional[int] = None # None = no upper bound allow_unauthenticated_participants: bool = False enable_join_access_control: bool = True - discovery_protection_kind: str = "SIGN" - liveliness_protection_kind: str = "SIGN" - rtps_protection_kind: str = "ENCRYPT" + discovery_protection_kind: str = "NONE" + liveliness_protection_kind: str = "NONE" + rtps_protection_kind: str = "ENCRYPT_WITH_ORIGIN_AUTHENTICATION" + rtps_psk_protection_kind: str = "ENCRYPT" enable_key_revision: bool = True topic_rules: list[TopicRule] = field( default_factory=lambda: [ @@ -903,6 +904,7 @@ def _safe_render(template: Path, dest: Path, context: dict) -> None: "discovery_protection_kind": gov.discovery_protection_kind, "liveliness_protection_kind": gov.liveliness_protection_kind, "rtps_protection_kind": gov.rtps_protection_kind, + "rtps_psk_protection_kind": gov.rtps_psk_protection_kind, "enable_key_revision": gov.enable_key_revision, "connext_version": tree.connext_version or (0, 0, 0), "connext_version_str": _version_str(tree.connext_version), diff --git a/system_arch/security/setup_security.py b/system_arch/security/setup_security.py index 84afb97..88c3ab3 100755 --- a/system_arch/security/setup_security.py +++ b/system_arch/security/setup_security.py @@ -37,6 +37,7 @@ Permissions, PskSeed, SecurityTree, + TopicRule, detect_connext_version, scaffold_tree, ) @@ -61,10 +62,32 @@ name="OperationalDomain", issuer=TRUSTED_PERMISSIONS_CA, # Explicitly NONE: the reference architecture does not protect - # discovery or liveliness metadata (RTPS payload is encrypted). + # discovery or liveliness metadata (already protected through RTPS ENCRYPT_WITH_ORIGIN_AUTHENTICATION). discovery_protection_kind="NONE", liveliness_protection_kind="NONE", + rtps_protection_kind="ENCRYPT_WITH_ORIGIN_AUTHENTICATION", + rtps_psk_protection_kind="ENCRYPT", + topic_rules=[ + TopicRule( + topic_expression="t/Vitals", + metadata_protection_kind="ENCRYPT", + ), + TopicRule( + topic_expression="t/MotorControl", + metadata_protection_kind="ENCRYPT", + ), + TopicRule(topic_expression="*"), + TopicRule( + topic_expression="DDS:Security:LogTopicV2", + enable_write_access_control=False, + metadata_protection_kind="SIGN", + data_protection_kind="ENCRYPT", + ), + ], ), + psk_seeds=[ + PskSeed(filename="OperationalDomain.psk"), + ], permissions=[ Permissions( name="Arm", @@ -148,8 +171,35 @@ governance=Governance( name="TeleopWanDomain", issuer=TRUSTED_PERMISSIONS_CA, + # Explicitly NONE: the reference architecture does not protect + # discovery or liveliness metadata (already protected through RTPS ENCRYPT_WITH_ORIGIN_AUTHENTICATION). discovery_protection_kind="NONE", liveliness_protection_kind="NONE", + rtps_protection_kind="ENCRYPT_WITH_ORIGIN_AUTHENTICATION", + rtps_psk_protection_kind="ENCRYPT", + # WAN governance protects ALL topics with topic-level insider + # protection: a catch-all "*" rule with metadata_protection_kind=ENCRYPT + # encrypts the submessage metadata of every topic, so only + # participants with matching permissions can decrypt it. This is + # stricter than the OperationalDomain (LAN) governance, which only + # applies metadata ENCRYPT to t/Vitals and t/MotorControl. + # + # Rule order matters: DDS evaluates topic rules first-match, top-down, + # so the specific DDS:Security:LogTopicV2 rule must precede the "*" + # catch-all or it would be shadowed (and the secure log would lose its + # SIGN metadata / ENCRYPT data protection). + topic_rules=[ + TopicRule( + topic_expression="DDS:Security:LogTopicV2", + enable_write_access_control=False, + metadata_protection_kind="SIGN", + data_protection_kind="ENCRYPT", + ), + TopicRule( + topic_expression="*", + metadata_protection_kind="ENCRYPT", + ), + ], ), permissions=[ Permissions(name="RsActiveWan", issuer=TRUSTED_PERMISSIONS_CA), diff --git a/system_arch/security/templates/governance.xml.j2 b/system_arch/security/templates/governance.xml.j2 index b91c1a1..47deee6 100644 --- a/system_arch/security/templates/governance.xml.j2 +++ b/system_arch/security/templates/governance.xml.j2 @@ -26,6 +26,7 @@ inability to use the software. --> {{ discovery_protection_kind }} {{ liveliness_protection_kind }} {{ rtps_protection_kind }} + {{ rtps_psk_protection_kind }} {% if connext_version >= (7, 5, 0) %} {{ "true" if enable_key_revision else "false" }} {% endif %} From e8295e491ad54bb409c09afc43de2e6139a50e5b Mon Sep 17 00:00:00 2001 From: Jose Maria Lopez Vega Date: Fri, 26 Jun 2026 20:28:27 +0200 Subject: [PATCH 4/4] Updated threat setup_threat_security script to warn about skipped files. --- .../security/setup_threat_security.py | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/modules/04-security-threat/security/setup_threat_security.py b/modules/04-security-threat/security/setup_threat_security.py index 60629c4..a66dd2b 100755 --- a/modules/04-security-threat/security/setup_threat_security.py +++ b/modules/04-security-threat/security/setup_threat_security.py @@ -219,7 +219,7 @@ def main(): ) print(f"Security directory tree scaffolded under {MODULE_SECURITY_DIR}") else: - SECURITY_TREE.generate_artifacts( + summary = SECURITY_TREE.generate_artifacts( root=MODULE_SECURITY_DIR, force=args.force, strict=args.strict ) @@ -227,9 +227,17 @@ def main(): # These are signed by the TrustedIdentityCa (so the CA chain is # valid) but have notAfter in the past, causing Connext to reject # them at participant creation time. + expired_generated = 0 + expired_skipped = 0 for app_name in ("ThreatInjector", "ThreatExfiltrator"): id_dir = MODULE_SECURITY_DIR / "identity" / "security-threat" / app_name / app_name expired_cert = id_dir / "certs" / "TrustedIdentityCa" / "expired" / f"{app_name}.crt" + # Mirror generate_expired_identity's own skip logic so the summary + # reflects what it actually did. + if expired_cert.is_file() and not args.force: + expired_skipped += 1 + else: + expired_generated += 1 generate_expired_identity( key_path=id_dir / "private" / f"{app_name}.key", cnf=id_dir / f"{app_name}.cnf", @@ -256,7 +264,23 @@ def main(): force=args.force, ) - print("Threat security artifacts generated!") + total_generated = summary["total_generated"] + expired_generated + total_skipped = summary["total_skipped"] + expired_skipped + print( + "Threat security artifact generation complete: " + f"{total_generated} generated, " + f"{total_skipped} skipped, " + f"{summary['warnings']} validation warning(s)." + ) + print( + "Breakdown: " + f"CA certs {summary['ca_certs_generated']} generated/{summary['ca_certs_skipped']} skipped; " + f"signed governance {summary['signed_governance_generated']}/{summary['signed_governance_skipped']}; " + f"signed permissions {summary['signed_permissions_generated']}/{summary['signed_permissions_skipped']}; " + f"identity certs {summary['identity_certs_generated']}/{summary['identity_certs_skipped']}; " + f"PSK seeds {summary['psk_seeds_generated']}/{summary['psk_seeds_skipped']}; " + f"expired identity certs {expired_generated}/{expired_skipped}." + ) if __name__ == "__main__":