-
Notifications
You must be signed in to change notification settings - Fork 31
OPC UA + gateway hardening (security, reconnect replay, multi-alarm, secure profile) #485
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mfaferek93
wants to merge
11
commits into
main
Choose a base branch
from
feat/opcua-gateway-hardening
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+3,619
−183
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
9d0fbae
feat(gateway): add secure-by-default field profile and hardening chec…
mfaferek93 407f675
feat(opcua): add OPC-UA client security (policy, certs, user auth)
mfaferek93 bd8dec1
feat(opcua): active-condition reconnect replay and multi-alarm mapping
mfaferek93 584915e
fix(opcua,gateway): harden secure profile and replay reconcile
mfaferek93 c02e603
fix(opcua,gateway,docs): address review and CI findings
mfaferek93 35a8cf9
fix(opcua): harden active-condition replay correctness
mfaferek93 e502a1e
test(opcua): add secured A&C integration test over a real SecureChannel
mfaferek93 82a5e25
fix(opcua): make read-replay reconcile guard airtight for transient r…
mfaferek93 cea9d15
test(opcua): fix flake8 lint in secured integration test
mfaferek93 80e2cfe
fix(opcua): harden A&C replay, security config, and fault de-dup
mfaferek93 b44ab2c
docs(opcua): drop misleading TOFU label from accept-any cert example
mfaferek93 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
117 changes: 117 additions & 0 deletions
117
src/ros2_medkit_gateway/config/gateway_params.secure.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| # ROS 2 Medkit Gateway - secure field profile | ||
| # | ||
| # Hardened parameter preset for on-prem / plant-network (appliance) | ||
| # deployments. It turns ON every control that the default development config | ||
| # leaves OFF: JWT auth, TLS, restricted CORS, and rate limiting. Use this file | ||
| # instead of gateway_params.yaml for any deployment reachable from an | ||
| # untrusted network. | ||
| # | ||
| # ros2 run ros2_medkit_gateway gateway_node \ | ||
| # --ros-args --params-file gateway_params.secure.yaml | ||
| # | ||
| # Items marked "REQUIRED" below must be set before the gateway will start / | ||
| # accept clients. See design/hardening.rst for the full checklist and | ||
| # credential / certificate provisioning steps. | ||
|
|
||
| ros2_medkit_gateway: | ||
| ros__parameters: | ||
| server: | ||
| # Appliance binds all interfaces; TLS + auth below protect the surface. | ||
| # Narrow to a management interface IP when the deployment allows it. | ||
| host: "0.0.0.0" | ||
| port: 8443 | ||
|
|
||
| # TLS/HTTPS - REQUIRED. Provision a real certificate (see | ||
| # scripts/generate_dev_certs.sh for the dev-only equivalent). The key | ||
| # file must be chmod 600 and owned by the gateway service user. | ||
| tls: | ||
| enabled: true | ||
| cert_file: "/etc/ros2_medkit/certs/cert.pem" # REQUIRED | ||
| key_file: "/etc/ros2_medkit/certs/key.pem" # REQUIRED (chmod 600) | ||
| ca_file: "" | ||
| # 1.3 preferred on a controlled fleet; drop to 1.2 only for legacy | ||
| # clients that cannot negotiate 1.3. | ||
| min_version: "1.3" | ||
|
|
||
| # CORS - restrict to the explicit origins that serve the operator UI. | ||
| # Never use ["*"] with credentials. Empty list disables CORS entirely | ||
| # (correct for API-only appliances with no browser UI). | ||
| cors: | ||
| allowed_origins: ["https://medkit-ui.local"] # REQUIRED if a browser UI is used; else [""] | ||
| allowed_methods: ["GET", "PUT", "POST", "DELETE", "OPTIONS"] | ||
| allowed_headers: ["Content-Type", "Accept", "Authorization"] | ||
| allow_credentials: true | ||
| max_age_seconds: 600 | ||
|
|
||
| # Authentication (JWT + RBAC) - REQUIRED on. | ||
| auth: | ||
| enabled: true | ||
| # HS256 shared secret (>= 32 chars) or, for RS256, the private key path. | ||
| # REQUIRED - inject from a secret store / env at deploy time; do NOT | ||
| # commit a real secret to source control. | ||
| jwt_secret: "" # REQUIRED | ||
| jwt_public_key: "" | ||
| jwt_algorithm: "HS256" | ||
| token_expiry_seconds: 3600 | ||
| refresh_token_expiry_seconds: 86400 | ||
| # "all" forces auth on every request (reads + writes). Use "write" only | ||
| # when unauthenticated reads are explicitly acceptable on this network. | ||
| require_auth_for: "all" | ||
| issuer: "ros2_medkit_gateway" | ||
| # Provision the minimum set of role-scoped clients. Format: | ||
| # "client_id:client_secret:role" (roles: viewer/operator/configurator/admin). | ||
| # REQUIRED - replace with real, rotated credentials. | ||
| clients: [""] # REQUIRED | ||
|
|
||
| # Rate limiting - ON to bound abuse / runaway clients. | ||
| rate_limiting: | ||
| enabled: true | ||
| global_requests_per_minute: 600 | ||
| client_requests_per_minute: 120 | ||
| # Tighten mutating endpoints (operations / data writes). | ||
| endpoint_limits: ["/api/v1/*/operations/*:30"] | ||
| client_cleanup_interval_seconds: 300 | ||
| client_max_idle_seconds: 600 | ||
|
|
||
| # Diagnostic scripts - disabled by default on an appliance. Enable | ||
| # deliberately and keep uploads off (manifest-defined scripts only). | ||
| scripts: | ||
| scripts_dir: "" | ||
| allow_uploads: false | ||
| max_file_size_mb: 10 | ||
| max_concurrent_executions: 5 | ||
| default_timeout_sec: 300 | ||
| max_execution_history: 100 | ||
|
|
||
| # Bulk data uploads - cap the payload size; raise only if the deployment | ||
| # genuinely needs large uploads. | ||
| bulk_data: | ||
| storage_dir: "/var/lib/ros2_medkit/bulk_data" | ||
| max_upload_size: 26214400 # 25 MiB | ||
| categories: [""] | ||
|
|
||
| # SOVD resource locking on, so concurrent operators cannot stamp on each | ||
| # other's mutations. | ||
| locking: | ||
| enabled: true | ||
| default_max_expiration: 3600 | ||
| cleanup_interval: 30 | ||
| defaults: | ||
| components: | ||
| lock_required_scopes: ["operations"] | ||
| breakable: true | ||
| apps: | ||
| lock_required_scopes: ["operations"] | ||
| breakable: true | ||
|
|
||
| # OpenAPI /docs endpoints off on a hardened appliance (reduce surface). | ||
| docs: | ||
| enabled: false | ||
|
|
||
| # Peer aggregation: if used across hosts, require TLS and do NOT forward | ||
| # client tokens to mDNS-discovered peers unless every peer is trusted. | ||
| aggregation: | ||
| enabled: false | ||
| require_tls: true | ||
| forward_auth: false | ||
| peer_scheme: "https" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| Gateway hardening (secure field profile) | ||
| ======================================== | ||
|
|
||
| The gateway ships every transport and access control needed for a hardened | ||
| deployment - JWT authentication with RBAC, TLS/HTTPS, restricted CORS, and | ||
| token-bucket rate limiting - but they are **disabled by default** so local | ||
| development works out of the box. A gateway exposed on a plant network with the | ||
| defaults is wide open: unauthenticated reads and writes over cleartext HTTP. | ||
|
|
||
| For any deployment reachable from an untrusted network, start from the secure | ||
| field profile preset ``config/gateway_params.secure.yaml`` instead of | ||
| ``config/gateway_params.yaml``: | ||
|
|
||
| .. code-block:: bash | ||
|
|
||
| ros2 run ros2_medkit_gateway gateway_node \ | ||
| --ros-args --params-file gateway_params.secure.yaml \ | ||
| -p auth.jwt_secret:="$MEDKIT_JWT_SECRET" \ | ||
| -p 'auth.clients:=["operator:'"$OP_SECRET"':operator"]' | ||
|
|
||
| What the secure profile turns on | ||
| -------------------------------- | ||
|
|
||
| ================================ ============== =========================================== | ||
| Control Default Secure profile | ||
| ================================ ============== =========================================== | ||
| ``auth.enabled`` false true | ||
| ``auth.require_auth_for`` write all (auth on reads + writes) | ||
| ``server.tls.enabled`` false true (HTTPS, min TLS 1.3) | ||
| ``cors.allowed_origins`` ``[]`` explicit origin list (no wildcard) | ||
| ``rate_limiting.enabled`` false true (global + per-client + per-endpoint) | ||
| ``scripts.allow_uploads`` true false (manifest-defined scripts only) | ||
| ``docs.enabled`` true false (reduced surface) | ||
| ``bulk_data.max_upload_size`` 100 MiB 25 MiB | ||
| ``locking`` on operations none lock required before mutation | ||
| ================================ ============== =========================================== | ||
|
|
||
| Credential and certificate provisioning | ||
| ---------------------------------------- | ||
|
|
||
| 1. **TLS certificate.** Provision a real server certificate + private key and | ||
| point ``server.tls.cert_file`` / ``server.tls.key_file`` at them. The key | ||
| file must be ``chmod 600`` and owned by the gateway service user. For a | ||
| dev/test box only, ``scripts/generate_dev_certs.sh`` emits a self-signed | ||
| ``cert.pem`` / ``key.pem`` / ``ca.pem`` (never use these in production). | ||
|
|
||
| 2. **JWT secret.** Generate a high-entropy secret of at least 32 characters | ||
| (HS256) or provision an RS256 key pair. Inject it at deploy time from a | ||
| secret store or environment variable - do not commit it to source control. | ||
|
|
||
| .. warning:: | ||
|
|
||
| ``auth.jwt_secret`` is a plain readable ROS 2 parameter. Beyond source | ||
| control it is exposed on two planes: | ||
|
|
||
| - **DDS control plane.** Any peer on the ROS 2 graph can read it with | ||
| ``ros2 param get /<gateway_node> auth.jwt_secret`` - the parameter is | ||
| declared readable and the DDS domain is unauthenticated by default. | ||
| - **Process table.** Passing it inline (``-p auth.jwt_secret:=...`` as in | ||
| the launch example above) also leaks the value via ``ps`` and | ||
| ``/proc/<pid>/cmdline``. | ||
|
|
||
| Injecting from an environment variable / params file instead of an inline | ||
| ``-p`` closes the process-table leak, but the value still lands in a | ||
| readable parameter, so it remains exposed on the DDS plane. To close that, | ||
| lock down the control plane: ROS 2 security (SROS2) with an access-control | ||
| policy that denies parameter reads to untrusted participants, or a | ||
| dedicated / firewalled ``ROS_DOMAIN_ID`` (optionally with | ||
| ``ROS_LOCALHOST_ONLY=1``) that no untrusted peer can join. | ||
|
|
||
| 3. **Role-scoped clients.** Create the minimum set of clients in | ||
| ``auth.clients`` (``client_id:client_secret:role``). Roles, least to most | ||
| privileged: ``viewer`` (read), ``operator`` (+ trigger ops / ack faults / | ||
| publish), ``configurator`` (+ modify configs), ``admin`` (+ auth | ||
| management). Rotate secrets periodically. | ||
|
|
||
| 4. **Obtain a token** and call the API over HTTPS: | ||
|
|
||
| .. code-block:: bash | ||
|
|
||
| curl -sk -X POST https://gateway:8443/api/v1/auth/authorize \ | ||
| -H 'Content-Type: application/json' \ | ||
| -d '{"client_id":"operator","client_secret":"...","grant_type":"client_credentials"}' | ||
| # use the returned access_token: | ||
| curl -sk https://gateway:8443/api/v1/faults -H "Authorization: Bearer $TOKEN" | ||
|
|
||
| Hardening checklist | ||
| ------------------- | ||
|
|
||
| Before exposing a gateway on a shared / plant network, confirm: | ||
|
|
||
| - [ ] ``auth.enabled: true`` and ``auth.require_auth_for`` is ``all`` (or | ||
| ``write`` only if unauthenticated reads are explicitly acceptable). | ||
| - [ ] ``auth.jwt_secret`` is set to a >= 32-char secret injected from a secret | ||
| store (not the placeholder, not in version control). | ||
| - [ ] ``auth.clients`` lists only the role-scoped clients you need; default / | ||
| example credentials removed; secrets rotated. | ||
| - [ ] ``server.tls.enabled: true`` with a real certificate; private key is | ||
| ``chmod 600``; ``min_version`` is ``1.3`` (or ``1.2`` only for legacy | ||
| clients). | ||
| - [ ] ``cors.allowed_origins`` is an explicit list (no ``*``); ``*`` is never | ||
| combined with ``allow_credentials: true``. | ||
| - [ ] ``rate_limiting.enabled: true`` with per-client and mutating-endpoint | ||
| limits tuned to the deployment. | ||
| - [ ] ``scripts.allow_uploads: false`` unless remote script upload is a | ||
| required, reviewed capability. | ||
| - [ ] ``bulk_data.max_upload_size`` bounded to what the deployment needs. | ||
| - [ ] If peer aggregation is used: ``aggregation.require_tls: true`` and | ||
| ``forward_auth`` only enabled when every peer is trusted. | ||
| - [ ] Bind ``server.host`` to a management interface where the network layout | ||
| allows, and place the gateway behind the plant firewall / segmentation. | ||
| - [ ] Back the gateway with persistent storage on a volume with restricted | ||
| permissions (faults DB, triggers DB, rosbag snapshots). | ||
|
|
||
| OPC-UA plugin (southbound) hardening | ||
| ------------------------------------ | ||
|
|
||
| The gateway controls the northbound REST surface; the OPC-UA plugin controls | ||
| the southbound connection to the PLC. Harden both. The plugin supports | ||
| SecurityPolicy (Basic256Sha256 / Aes128 / Aes256), MessageSecurityMode | ||
| (Sign / SignAndEncrypt), a client application-instance certificate, a server | ||
| trust store with reject-untrusted, and user identity (anonymous / | ||
| username-password / X.509). See ``ros2_medkit_opcua`` README, section | ||
| "OPC-UA client security". | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.