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:
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 |
|---|---|---|
|
false |
true |
|
write |
all (auth on reads + writes) |
|
false |
true (HTTPS, min TLS 1.3) |
|
|
explicit origin list (no wildcard) |
|
false |
true (global + per-client + per-endpoint) |
|
true |
false (manifest-defined scripts only) |
|
true |
false (reduced surface) |
|
100 MiB |
25 MiB |
|
none |
lock required before mutation |
Credential and certificate provisioning
TLS certificate. Provision a real server certificate + private key and point
server.tls.cert_file/server.tls.key_fileat them. The key file must bechmod 600and owned by the gateway service user. For a dev/test box only,scripts/generate_dev_certs.shemits a self-signedcert.pem/key.pem/ca.pem(never use these in production).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_secretis 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 viapsand/proc/<pid>/cmdline.
Injecting from an environment variable / params file instead of an inline
-pcloses 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 / firewalledROS_DOMAIN_ID(optionally withROS_LOCALHOST_ONLY=1) that no untrusted peer can join.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.Obtain a token and call the API over HTTPS:
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: trueandauth.require_auth_forisall(orwriteonly if unauthenticated reads are explicitly acceptable).[ ]
auth.jwt_secretis set to a >= 32-char secret injected from a secret store (not the placeholder, not in version control).[ ]
auth.clientslists only the role-scoped clients you need; default / example credentials removed; secrets rotated.[ ]
server.tls.enabled: truewith a real certificate; private key ischmod 600;min_versionis1.3(or1.2only for legacy clients).[ ]
cors.allowed_originsis an explicit list (no*);*is never combined withallow_credentials: true.[ ]
rate_limiting.enabled: truewith per-client and mutating-endpoint limits tuned to the deployment.[ ]
scripts.allow_uploads: falseunless remote script upload is a required, reviewed capability.[ ]
bulk_data.max_upload_sizebounded to what the deployment needs.[ ] If peer aggregation is used:
aggregation.require_tls: trueandforward_authonly enabled when every peer is trusted.[ ] Bind
server.hostto 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”.