Skip to content

Commit cda80ca

Browse files
committed
Enhacement on security
Signed-off-by: susanachl <susanachavez8@hotmail.com>
1 parent c825303 commit cda80ca

12 files changed

Lines changed: 433 additions & 47 deletions

File tree

.github/workflows/ros_ci.yml

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,28 @@ jobs:
1414
1515
- name: Run build and tests inside container
1616
run: |
17-
docker run --rm dicom_to_ros:ci bash -c "
18-
source /opt/ros/jazzy/setup.bash &&
19-
colcon build &&
20-
colcon test --event-handlers=console_direct+ &&
21-
colcon test-result
22-
"
17+
docker run --rm \
18+
--workdir /ros2_ws \
19+
-v ${{ github.workspace }}/dicom_interfaces:/ros2_ws/src/dicom_interfaces \
20+
-v ${{ github.workspace }}/dicom_to_ros:/ros2_ws/src/dicom_to_ros \
21+
-v keystore_ci:/ros2_ws/keystore \
22+
-e ROS_SECURITY_ENABLE=true \
23+
-e ROS_SECURITY_STRATEGY=Enforce \
24+
-e ROS_SECURITY_KEYSTORE=/ros2_ws/keystore \
25+
dicom_to_ros:ci bash -c "
26+
set -e
27+
echo '--- Generating Keystore ---'
28+
./src/dicom_to_ros/dicom_to_ros/security/generate_keystore.sh
29+
30+
echo '--- Building Workspace ---'
31+
source /opt/ros/jazzy/setup.bash
32+
colcon build --symlink-install
33+
34+
echo '--- Running Tests ---'
35+
source ./install/setup.bash
36+
colcon test --event-handlers=console_direct+
37+
colcon test-result
38+
"
39+
40+
volumes:
41+
keystore_ci:

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ build/
1212
install/
1313
log/
1414

15+
# ROS 2 Security
16+
keystore/
17+
sros2_keystore/
18+
19+
# Environment variables
20+
.env
21+
1522
# IDE / Editor
1623
.vscode/
1724
.idea/

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ The `docker/` directory runs the ROS 2 DICOM listener and all processing nodes.
167167
git clone https://github.com/Ekumen-OS/dicom_to_ros.git
168168
cd dicom_to_ros
169169

170+
export UID=$(id -u)
170171
export GID=$(id -g)
171172
docker compose -f docker/docker-compose.yml up --build -d
172173
```
@@ -182,6 +183,60 @@ The pipeline starts immediately and listens for DICOM C-STORE requests on port `
182183

183184
---
184185

186+
## Security
187+
188+
This project supports **ROS 2 Security (SROS2)** to protect sensitive patient data transmitted over the network. When enabled, SROS2 provides authentication, encryption, and access control for all internal topics, including the raw `/dicom_interfaces/Dicom` message and the derived `/dicom_study_info` topic.
189+
190+
### Enabling Security
191+
192+
Security is enabled by default in the provided `docker-compose.yml` file via environment variables. On the first run, a script generates a security keystore containing the necessary keys and certificates for all pipeline nodes. This keystore is persisted in a Docker volume named `keystore`.
193+
194+
> **Note:** The initial key generation is a one-time process. If the container is stopped during this first run, the security volume may be left in a corrupted state. If this happens, you will need to manually remove the Docker volume (`docker volume rm <project>_keystore`) before restarting the container.
195+
196+
### Accessing Secured Topics from Your Application
197+
198+
By default, only the internal `dicom_to_ros` nodes can communicate. To grant your own ROS 2 node access to a secured topic (e.g., `/dicom_study_info`):
199+
200+
1. **Add a Profile to the Policy:** Open `dicom_to_ros/dicom_to_ros/security/permissions.xml` and add a new `<profile>` for your node inside the `<profiles>` block. You must grant it permissions to the system topics (`/rosout`, `/parameter_events`) and services (`~/*`) in addition to any data topics.
201+
202+
*Example for a node named `my_subscriber` that needs to read study info:*
203+
```xml
204+
<profile ns="/dicom_to_ros" node="my_subscriber">
205+
<topics subscribe="ALLOW">
206+
<topic>/dicom_to_ros/dicom_study_info</topic>
207+
<topic>/parameter_events</topic>
208+
</topics>
209+
<topics publish="ALLOW">
210+
<topic>/rosout</topic>
211+
<topic>/parameter_events</topic>
212+
</topics>
213+
<services reply="ALLOW">
214+
<service>~/*</service>
215+
</services>
216+
</profile>
217+
```
218+
2. **Regenerate the Keystore:** The security artifacts must be regenerated to include your new node. Bring down the pipeline and remove the old keystore volume.
219+
```bash
220+
docker compose -f docker/docker-compose.yml down -v
221+
```
222+
3. **Restart the Pipeline:** The entrypoint script will automatically generate a new keystore that includes keys and permissions for your node.
223+
```bash
224+
docker compose -f docker/docker-compose.yml up -d
225+
```
226+
4. **Launch Your Node:** When you launch your application, ensure it joins the correct namespace and enclave.
227+
```python
228+
# Example in a Python launch file
229+
Node(
230+
package='my_package',
231+
executable='my_subscriber_node',
232+
name='my_subscriber',
233+
namespace='/dicom_to_ros',
234+
ros_arguments=['--enclave', '/dicom_to_ros']
235+
)
236+
```
237+
238+
---
239+
185240
## Demo
186241

187242
For a more detailed demo, please check the [dicom_to_ros_demo](dicom_to_ros_demo/README.md) folder.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# Get the directory where this script is located to find permissions.xml
5+
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
6+
7+
# This script generates the ROS 2 security keystore and keys for all nodes
8+
# in the dicom_to_ros pipeline.
9+
10+
11+
KEYSTORE_DIR="/ros2_ws/keystore"
12+
13+
# Check if the final permissions file exists and if the policy has not changed.
14+
POLICY_FILE="$SCRIPT_DIR/permissions.xml"
15+
CHECKSUM_FILE="$KEYSTORE_DIR/permissions.md5"
16+
17+
if [ -f "$KEYSTORE_DIR/enclaves/dicom_to_ros/permissions.p7s" ]; then
18+
if [ -f "$CHECKSUM_FILE" ] && md5sum -c --status "$CHECKSUM_FILE"; then
19+
echo "Complete and up-to-date keystore found. Skipping generation."
20+
exit 0
21+
else
22+
echo "Permissions policy has changed or checksum is missing. Regenerating keystore..."
23+
rm -rf "$KEYSTORE_DIR"/*
24+
fi
25+
fi
26+
27+
# If the keystore is incomplete, check if the directory is not empty.
28+
# An incomplete keystore in a non-empty directory represents a corrupted state.
29+
if [ -d "$KEYSTORE_DIR" ] && [ "$(ls -A $KEYSTORE_DIR)" ]; then
30+
echo "############################################################################" >&2
31+
echo "## ERROR: Incomplete or Corrupted Security Keystore Detected! ##" >&2
32+
echo "############################################################################" >&2
33+
echo "This can happen if the container was stopped during initial key generation." >&2
34+
echo "" >&2
35+
echo "To fix this, please run the following commands from your host machine:" >&2
36+
echo " 1. docker compose -f docker/docker-compose.yml down" >&2
37+
echo " 2. docker volume rm <project-name>_keystore" >&2
38+
echo " (e.g., 'docker volume rm docker_keystore' if you ran compose from the 'docker' directory)" >&2
39+
echo " 3. Restart the container." >&2
40+
echo "############################################################################" >&2
41+
exit 1
42+
fi
43+
44+
echo "Empty keystore volume detected. Generating fresh security artifacts..."
45+
46+
# WORKAROUND: `create_keystore` requires the target directory to not exist,
47+
# but we can't remove the volume mount point. So, we create it in a temporary
48+
# location and then move the contents into the empty volume.
49+
TMP_KEYSTORE_DIR=$(mktemp -d)
50+
ros2 security create_keystore "$TMP_KEYSTORE_DIR"
51+
52+
# Move the generated CA and initial files into the actual keystore volume
53+
mv "$TMP_KEYSTORE_DIR"/* "$KEYSTORE_DIR"/
54+
rm -rf "$TMP_KEYSTORE_DIR"
55+
56+
ENCLAVE_NAME="/dicom_to_ros"
57+
58+
# The `create_enclave` command in Jazzy is designed to create the enclave
59+
# and all node keys defined in the policy file in a single step.
60+
# In ROS 2 Jazzy, the `create_enclave` command is intended to generate all
61+
# keys and permissions from a policy file in a single step.
62+
echo "Creating enclave and generating keys for all nodes from policy file..."
63+
64+
# The 'create_enclave' command and its arguments have changed in recent ROS 2 versions.
65+
# The modern approach is to create keys for each node individually and then
66+
# create the signed permissions file for the enclave.
67+
echo "Creating enclave and generating keys for all nodes from policy file..."
68+
# The modern `create_enclave` command handles generating all necessary keys and certificates.
69+
ros2 security create_enclave "$KEYSTORE_DIR" "$ENCLAVE_NAME"
70+
71+
# The `create_permission` command then signs the policy file using the generated enclave certificate.
72+
echo "Creating signed permissions for enclave: $ENCLAVE_NAME"
73+
ros2 security create_permission "$KEYSTORE_DIR" "$ENCLAVE_NAME" "$SCRIPT_DIR/permissions.xml"
74+
75+
# Store a checksum of the policy file to detect future changes.
76+
md5sum "$POLICY_FILE" > "$CHECKSUM_FILE"
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<policy version="0.2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="ros_security_policy.xsd">
3+
<enclaves>
4+
<enclave path="/dicom_to_ros">
5+
<!--
6+
This profile grants permissions to the core nodes of the dicom_to_ros pipeline.
7+
It follows a principle of least privilege, where each node only has access
8+
to the topics it needs to function.
9+
-->
10+
<profiles>
11+
<profile ns="/dicom_to_ros" node="dicom_server">
12+
<topics publish="ALLOW">
13+
<topic>/dicom_interfaces/Dicom</topic>
14+
<topic>/rosout</topic>
15+
<topic>/parameter_events</topic>
16+
</topics>
17+
<topics subscribe="ALLOW">
18+
<topic>/parameter_events</topic>
19+
</topics>
20+
<services reply="ALLOW">
21+
<service>~/*</service>
22+
</services>
23+
</profile>
24+
25+
<profile ns="/dicom_to_ros" node="dicom2studyinfo">
26+
<topics subscribe="ALLOW">
27+
<topic>/dicom_interfaces/Dicom</topic>
28+
<topic>/parameter_events</topic>
29+
</topics>
30+
<topics publish="ALLOW">
31+
<topic>/dicom_to_ros/dicom_study_info</topic>
32+
<topic>/rosout</topic>
33+
<topic>/parameter_events</topic>
34+
</topics>
35+
<services reply="ALLOW">
36+
<service>~/*</service>
37+
</services>
38+
</profile>
39+
40+
<profile ns="/dicom_to_ros" node="dicom2img">
41+
<topics subscribe="ALLOW">
42+
<topic>/dicom_interfaces/Dicom</topic>
43+
<topic>/parameter_events</topic>
44+
</topics>
45+
<topics publish="ALLOW">
46+
<topic>/dicom_to_ros/dicom_image</topic>
47+
<topic>/dicom_to_ros/dicom_camera_info</topic>
48+
<topic>/rosout</topic>
49+
<topic>/parameter_events</topic>
50+
</topics>
51+
<services reply="ALLOW">
52+
<service>~/*</service>
53+
</services>
54+
</profile>
55+
56+
<profile ns="/dicom_to_ros" node="dicom2video">
57+
<topics subscribe="ALLOW">
58+
<topic>/dicom_interfaces/Dicom</topic>
59+
<topic>/parameter_events</topic>
60+
</topics>
61+
<topics publish="ALLOW">
62+
<topic>/dicom_to_ros/dicom_video_frames</topic>
63+
<topic>/dicom_to_ros/dicom_video_camera_info</topic>
64+
<topic>/rosout</topic>
65+
<topic>/parameter_events</topic>
66+
</topics>
67+
<services reply="ALLOW">
68+
<service>~/*</service>
69+
</services>
70+
</profile>
71+
72+
<profile ns="/dicom_to_ros" node="dicom2pcl">
73+
<topics subscribe="ALLOW">
74+
<topic>/dicom_interfaces/Dicom</topic>
75+
<topic>/parameter_events</topic>
76+
</topics>
77+
<topics publish="ALLOW">
78+
<topic>/dicom_to_ros/dicom_point_cloud</topic>
79+
<topic>/rosout</topic>
80+
<topic>/parameter_events</topic>
81+
</topics>
82+
<services reply="ALLOW">
83+
<service>~/*</service>
84+
</services>
85+
</profile>
86+
87+
<profile ns="/dicom_to_ros" node="dicom2tf">
88+
<topics subscribe="ALLOW">
89+
<topic>/dicom_interfaces/Dicom</topic>
90+
<topic>/parameter_events</topic>
91+
</topics>
92+
<topics publish="ALLOW">
93+
<topic>/tf</topic>
94+
<topic>/rosout</topic>
95+
<topic>/parameter_events</topic>
96+
</topics>
97+
<services reply="ALLOW">
98+
<service>~/*</service>
99+
</services>
100+
</profile>
101+
102+
<!-- Allow RViz's dynamically generated hidden TF nodes -->
103+
<!-- 1. RViz Main Node -->
104+
<profile ns="/dicom_to_ros" node="rviz2">
105+
<topics subscribe="ALLOW">
106+
<topic>/dicom_to_ros/dicom_image</topic>
107+
<topic>/dicom_to_ros/dicom_video_frames</topic>
108+
<topic>/dicom_to_ros/dicom_point_cloud</topic>
109+
<topic>/tf</topic>
110+
<topic>/tf_static</topic>
111+
<topic>/parameter_events</topic>
112+
</topics>
113+
<topics publish="ALLOW">
114+
<topic>/rosout</topic>
115+
<topic>/parameter_events</topic>
116+
117+
<topic>/initialpose</topic>
118+
<topic>/goal_pose</topic>
119+
<topic>/clicked_point</topic>
120+
121+
<topic>/dicom_to_ros/initialpose</topic>
122+
<topic>/dicom_to_ros/goal_pose</topic>
123+
<topic>/dicom_to_ros/clicked_point</topic>
124+
</topics>
125+
<services reply="ALLOW">
126+
<service>/*</service>
127+
</services>
128+
<services request="ALLOW">
129+
<service>/*</service>
130+
</services>
131+
</profile>
132+
133+
<!-- 2. RViz Hidden TF Nodes -->
134+
<profile ns="/dicom_to_ros" node="transform_listener_impl_*">
135+
<topics subscribe="ALLOW">
136+
<topic>/tf</topic>
137+
<topic>/tf_static</topic>
138+
<topic>/parameter_events</topic>
139+
</topics>
140+
<topics publish="ALLOW">
141+
<topic>/rosout</topic>
142+
<topic>/parameter_events</topic>
143+
</topics>
144+
<services reply="ALLOW">
145+
<service>/*</service>
146+
</services>
147+
<services request="ALLOW">
148+
<service>/*</service>
149+
</services>
150+
</profile>
151+
</profiles>
152+
153+
</enclave>
154+
</enclaves>
155+
</policy>

0 commit comments

Comments
 (0)