Skip to content
Start free evaluation

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 roles and null_role policy applied to every request.
  • See how a host name mismatch, a plain client, a mode mismatch and a revoked certificate fail.
  • A PDComm Modbus SDK that includes Security (capabilities reports security as true) and the C++ console from the quickstart
  • The OpenSSL 3 command-line tool: openssl version must start with OpenSSL 3. On macOS the built-in openssl is LibreSSL, so use Homebrew’s openssl@3.
  • Two terminals

Create a certs folder in your working folder and a CA in it:

터미널 창
mkdir certs && cd certs
openssl 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.pem

Issue 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
  • -extfile adds the SAN, key usage and role extensions. PDComm accepts X.509v3 device certificates only.
  • Each *-chain.pem is 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:
..reader

The .. 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.

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)

In terminal B, start the C++ console:

터미널 창
./build/modbus-tutorial/pdcomm-modbus-tutorial

Connect 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.

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.

Revoke the reader certificate and create a CRL. Create ca.cnf in the certs folder with this content:

certs/ca.cnf
[ca]
default_ca = lab
[lab]
database = index.txt
crlnumber = crlnumber
certificate = ca.pem
private_key = ca.key
default_md = sha256
default_crl_days = 30
터미널 창
cd certs
touch index.txt && echo 01 > crlnumber
openssl ca -config ca.cnf -revoke reader.pem
openssl ca -config ca.cnf -gencrl -out ca.crl
cd ..

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":true

Connect 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").

  1. Terminal B: quit
  2. Terminal A: {"action":"shutdown"}
  3. When you’re done testing, delete the certs folder.