Modbus/TCP Security
In this tutorial you run a Modbus/TCP Security server and client on one computer. You create a test certificate authority (CA) and client certificates with different roles, then check that the server allows or denies function codes by role.
- Create client certificates that carry the role extension (OID
1.3.6.1.4.1.50316.802.1). - See the server’s
rolesandnull_rolepolicy applied to every request. - See how a host name mismatch, a plain client, a mode mismatch and a revoked certificate fail.
What you need
Section titled “What you need”- A PDComm Modbus SDK that includes Security (
capabilitiesreportssecurityastrue) and the C++ console from the quickstart - The OpenSSL 3 command-line tool:
openssl versionmust start withOpenSSL 3.On macOS the built-inopensslis LibreSSL, so use Homebrew’sopenssl@3. - Two terminals
1. Create the certificates
Section titled “1. Create the certificates”Create a certs folder in your working folder and a CA in it:
mkdir certs && cd certsopenssl req -x509 -newkey rsa:2048 -nodes -sha256 -days 365 -subj "/CN=Modbus Lab CA" \ -addext "basicConstraints=critical,CA:TRUE" -addext "keyUsage=critical,keyCertSign,cRLSign" \ -keyout ca.key -out ca.pemIssue certificates for one server and three clients (reader, operator and one without a role). Every certificate gets localhost and 127.0.0.1 in its SAN, and reader and operator get the role extension:
for name in server reader operator no-role; do { echo "basicConstraints=critical,CA:FALSE" echo "keyUsage=critical,digitalSignature,keyEncipherment" echo "extendedKeyUsage=serverAuth,clientAuth" echo "subjectAltName=DNS:localhost,IP:127.0.0.1" case "$name" in reader|operator) echo "1.3.6.1.4.1.50316.802.1=ASN1:UTF8String:$name" ;; esac } > "$name.ext" openssl req -new -newkey rsa:2048 -nodes -subj "/CN=$name" -keyout "$name.key" -out "$name.csr" openssl x509 -req -in "$name.csr" -CA ca.pem -CAkey ca.key -CAcreateserial -days 365 -sha256 \ -extfile "$name.ext" -out "$name.pem" cat "$name.pem" ca.pem > "$name-chain.pem"done-extfileadds the SAN, key usage and role extensions. PDComm accepts X.509v3 device certificates only.- Each
*-chain.pemis the certificate followed by the root CA. PDComm requires a chain that ends with the root CA.
Check the role extension:
openssl x509 -in reader.pem -noout -text | grep -A1 '50316' 1.3.6.1.4.1.50316.802.1: ..readerThe .. is the UTF8String’s format bytes shown as text. Go back to the working folder (cd ..). In the following commands, replace /absolute/path/certs with the absolute path of this folder.
2. Open the TLS server
Section titled “2. Open the TLS server”In terminal A, start the headless simulator and open the secure server:
"$MODBUS_SDK/bin/pdcomm-modbus-headless"{"action":"configure","config":{"transport":"tcp","role":"server","address":"127.0.0.1:1802","port":"","unit":1,"baud":19200,"parity":"even","stop_bits":1,"timeout_ms":1000,"security":{"mode":"encrypted","chain":"/absolute/path/certs/server-chain.pem","private_key":"/absolute/path/certs/server.key","trust":"/absolute/path/certs/ca.pem","roles":{"reader":[1,2,3,4],"operator":[3,6,16]},"null_role":[]}}}{"action":"connect"}The authorization policy:
| Client | Allowed functions |
|---|---|
Role reader |
01, 02, 03, 04 |
Role operator |
03, 06, 16 |
| No role | None (null_role is empty) |
3. Send requests with each role
Section titled “3. Send requests with each role”In terminal B, start the C++ console:
./build/modbus-tutorial/pdcomm-modbus-tutorialConnect as reader and send a read and a write:
{"action":"configure","config":{"transport":"tcp","role":"client","address":"127.0.0.1:1802","port":"","unit":1,"baud":19200,"parity":"even","stop_bits":1,"timeout_ms":1000,"security":{"mode":"encrypted","chain":"/absolute/path/certs/reader-chain.pem","private_key":"/absolute/path/certs/reader.key","trust":"/absolute/path/certs/ca.pem"}}}{"action":"connect"}{"action":"request","pdu":"0300000001"}{"action":"request","pdu":"060000002A"}{"action":"disconnect"}{"ok":true,"result":{"response":[3,2,0,0],"source":"wire","status":"success"}}{"ok":true,"result":{"response":[134,1],"source":"wire","status":"exception"}}reader can’t use function 06, so the write gets exception 01 ([134,1]). The connection stays open.
Connect again as operator: in configure, change only chain and private_key to operator-chain.pem and operator.key. Then:
{"action":"connect"}{"action":"request","pdu":"060000002A"}{"action":"request","pdu":"0300000001"}{"action":"request","pdu":"0400000001"}{"action":"disconnect"}{"ok":true,"result":{"response":[6,0,0,0,42],"source":"wire","status":"success"}}{"ok":true,"result":{"response":[3,2,0,42],"source":"wire","status":"success"}}{"ok":true,"result":{"response":[132,1],"source":"wire","status":"exception"}}operator may write and use 03, but 04 isn’t in its policy, so it gets exception 01.
With the certificate without a role (no-role-chain.pem, no-role.key), null_role is empty, so even 03 returns [131,1]. The TLS connection itself succeeds because the CA issued the certificate.
4. See connection failures
Section titled “4. See connection failures”In each of these cases connect or the first request fails. The C++ console prints Command error: and the error JSON on standard error.
| Change | Result |
|---|---|
Add "hostname":"plc.example" to the client security |
connect fails with Security("Handshake(\"error:0A000086:SSL routines:tls_post_process_server_certificate:certificate verify failed:...\")") |
Set the client security to null (plain TCP) |
connect succeeds, but request fails with Io(ConnectionReset, "Connection reset by peer (os error 54)") |
Set only the client to "mode":"authentication_only" |
connect fails with Security("Handshake(\"...ssl/tls alert handshake failure...SSL alert number 40\")") |
Use reader.pem, which lacks the root CA, as the client chain |
connect fails with Config("Chain must include its root CA") |
Use another certificate’s key as the client private_key |
connect fails with Crypto(ErrorStack([...reason: "key values mismatch"...])) |
- A name that isn’t in the server certificate’s SAN fails certificate validation. To reach a real device by IP address, put that IP into
hostname, and make sure the server certificate’s SAN contains it too. - PDComm never falls back to plain TCP or authentication-only mode when a secure connection fails.
5. Try authentication-only mode (optional)
Section titled “5. Try authentication-only mode (optional)”authentication_only provides authentication and integrity without encryption, and both sides must use it. In terminal A, disconnect the server:
{"action":"disconnect"}Enter the configure of step 2 again with "mode":"authentication_only", then connect. Connect the client with the operator configuration plus "mode":"authentication_only", and 0300000001 succeeds. This mode uses TLS 1.2 NULL-SHA256, which needs RSA certificates.
6. Block a revoked certificate (optional)
Section titled “6. Block a revoked certificate (optional)”Revoke the reader certificate and create a CRL. Create ca.cnf in the certs folder with this content:
[ca]default_ca = lab[lab]database = index.txtcrlnumber = crlnumbercertificate = ca.pemprivate_key = ca.keydefault_md = sha256default_crl_days = 30cd certstouch index.txt && echo 01 > crlnumberopenssl ca -config ca.cnf -revoke reader.pemopenssl ca -config ca.cnf -gencrl -out ca.crlcd ..Disconnect the server, add these two members to the security of step 2 and open it again:
"crls":["/absolute/path/certs/ca.crl"],"require_revocation":trueConnect as reader: connect succeeds, but the first request fails, because with TLS 1.3 the server validates the client certificate after the handshake completes.
Command error: {"error":"Io(Other, \"error:0A000414:SSL routines:ssl3_read_bytes:ssl/tls alert certificate revoked:...SSL alert number 44\")","ok":false}state then reports connected as false. Connecting as operator works. Setting require_revocation to true with an empty crls list fails with Config("Revocation checking requires CRLs").
7. Clean up
Section titled “7. Clean up”- Terminal B:
quit - Terminal A:
{"action":"shutdown"} - When you’re done testing, delete the
certsfolder.
Next steps
Section titled “Next steps”- Security connection problems explains each TLS error message.
- The simulator GUI shows how to enter the same JSON in the Security tab.