You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@plc4x.apache.org by sr...@apache.org on 2026/08/24 14:48:39 UTC
(plc4x) 23/24: fix(plc4j/opcua): sign and encrypt by default, and never send a password in the clear
This is an automated email from the ASF dual-hosted git repository.
sruehl pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x.git
commit 93cb9ee98b193660874d7bd644696327a91e70b9
Author: Sebastian Rühl <sr...@apache.org>
AuthorDate: Mon Aug 24 16:19:37 2026 +0200
fix(plc4j/opcua): sign and encrypt by default, and never send a password in the clear
The shipped default was a security policy of NONE: no signing, no encryption, and
no authentication of the server, so anything on the path could read and change what
was exchanged. It now defaults to Basic256Sha256, which needs the server's
certificate to be trusted - so a connection configuring neither trust-store-file
nor server-certificate-file fails where it used to come up unprotected. NONE
remains available, as does insecure-certificate-verification for a server whose
certificate cannot be anchored.
Separately, a username and password over a channel that neither signs nor encrypts
went out in the clear. Unlike a value, a password read once stays useful, so this
is refused unless allow-insecure-credentials says otherwise - and there is a test
that the opt-out works, because an error telling an operator to set something that
does not help them is worse than no error.
Nine driver tests needed the configuration they had never had to give. They were
written when the default protected nothing, so they named neither what to trust nor
what to sign with; a test connecting over a signing channel should say both. One
more asserts what happens with no trust anchor at all, so the address it builds on
had to stop supplying one.
Worth writing down for whoever meets it next: a signing channel also needs a client
key pair, and with no key-store-file the driver quietly generates a throwaway
self-signed one. That is harmless against a server which does not authenticate its
clients and useless against one that does - and after this change it is where every
connection without a key store ends up. The release notes say so; whether the
driver should be doing it at all is a separate question from this one.
---
RELEASE_NOTES | 29 +++++++++++++++++++
.../java/opcua/config/OpcuaConfiguration.java | 24 ++++++++++++++--
.../plc4x/java/opcua/context/SecureChannel.java | 12 ++++++++
.../plc4x/java/opcua/OpcuaPlcDriverTest.java | 22 +++++++++++++--
.../context/SecureChannelUserIdentityTest.java | 33 ++++++++++++++++++++++
5 files changed, 116 insertions(+), 4 deletions(-)
diff --git a/RELEASE_NOTES b/RELEASE_NOTES
index a875007ee3..74aee3c15b 100644
--- a/RELEASE_NOTES
+++ b/RELEASE_NOTES
@@ -160,6 +160,35 @@ Incompatible changes
PlcConnectionStateChangedEvent events which have many more
state change options beyond a simple connected and disconnected
event.
+- The OPC UA driver's "security-policy" now defaults to
+ Basic256Sha256 instead of NONE. NONE means the channel is
+ neither signed nor encrypted, so anything on the network path
+ can read and alter what is exchanged, and the server is not
+ authenticated at all. A signing and encrypting channel needs the
+ server's certificate to be trusted, so a connection that
+ configured neither "trust-store-file" nor
+ "server-certificate-file" will now fail where it previously came
+ up unprotected. Set one of those, or "security-policy=NONE" to
+ keep the old behaviour, or "insecure-certificate-verification=true"
+ to accept whatever certificate the server presents.
+ Note that a signing channel also needs a client key pair: supply
+ one with "key-store-file", or the driver generates a throwaway
+ self-signed certificate, which a server that authenticates its
+ clients will not accept.
+- The OPC UA driver refuses to send a username and password over a
+ channel that neither signs nor encrypts, because the password is
+ then readable by anything on the path and stays useful long after
+ it is read. The new "allow-insecure-credentials" parameter sends
+ them anyway, with a warning.
+- The OPC UA driver now requires an endpoint to match both the
+ requested security policy and the requested message security
+ mode, and selects the strongest endpoint that matches rather than
+ the weakest. It previously accepted an endpoint matching either
+ one and then chose the lowest security level on offer, so a
+ server publishing a wide-open endpoint beside a protected one was
+ usually reached over the wide-open one. Where two endpoints are
+ equally strong, one whose user token policy protects the token is
+ preferred.
- The ctrlX driver no longer trusts the Bosch factory default
certificate that ships inside the driver jar, and no longer
accepts a certificate for any host regardless of the name it was
diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/config/OpcuaConfiguration.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/config/OpcuaConfiguration.java
index 072b075afc..4c20e86c95 100644
--- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/config/OpcuaConfiguration.java
+++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/config/OpcuaConfiguration.java
@@ -64,9 +64,13 @@ public class OpcuaConfiguration implements Configuration {
private String password;
@ConfigurationParameter("security-policy")
- @StringDefaultValue("NONE")
+ @StringDefaultValue("Basic256Sha256")
@Description("The security policy applied to communication channel between driver and OPC UA server.\n" +
- "Default value assumes. Possible options are `NONE`, `Basic128Rsa15`, `Basic256`, `Basic256Sha256`, `Aes128_Sha256_RsaOaep`, `Aes256_Sha256_RsaPss`.")
+ "Possible options are `NONE`, `Basic128Rsa15`, `Basic256`, `Basic256Sha256`, `Aes128_Sha256_RsaOaep`, `Aes256_Sha256_RsaPss`.\n" +
+ "`NONE` means the channel is neither signed nor encrypted, so anything on the path can read and\n" +
+ "change what is exchanged; it also leaves the server unauthenticated. A policy that signs and\n" +
+ "encrypts needs a trust anchor for the server's certificate - see `trust-store-file` and\n" +
+ "`server-certificate-file`.")
private SecurityPolicy securityPolicy;
@ConfigurationParameter("message-security")
@@ -113,6 +117,13 @@ public class OpcuaConfiguration implements Configuration {
@Description("Password used to open trust store.")
private String trustStorePassword;
+ @ConfigurationParameter("allow-insecure-credentials")
+ @BooleanDefaultValue(false)
+ @Description("Allows a username and password to be sent over a channel that neither signs nor encrypts.\n" +
+ "Without this, a connection configured with credentials over an unprotected channel fails rather\n" +
+ "than putting the password on the wire where anything on the path can read it. Setting it warns.")
+ private boolean allowInsecureCredentials;
+
@ConfigurationParameter("insecure-certificate-verification")
@BooleanDefaultValue(false)
@Description("Disables verification of the OPC UA server certificate, trusting any certificate the server presents.\n" +
@@ -221,6 +232,10 @@ public class OpcuaConfiguration implements Configuration {
return trustStorePassword == null ? null : trustStorePassword.toCharArray();
}
+ public boolean isAllowInsecureCredentials() {
+ return allowInsecureCredentials;
+ }
+
public boolean isInsecureCertificateVerification() {
return insecureCertificateVerification;
}
@@ -254,6 +269,11 @@ public class OpcuaConfiguration implements Configuration {
return serverCertificate;
}
+ /** Visible for tests that cover how a token is built rather than whether sending it is wise. */
+ public void setAllowInsecureCredentials(boolean allowInsecureCredentials) {
+ this.allowInsecureCredentials = allowInsecureCredentials;
+ }
+
public void setServerCertificate(X509Certificate serverCertificate) {
this.serverCertificate = serverCertificate;
}
diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/SecureChannel.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/SecureChannel.java
index 7780f69004..b792870a55 100644
--- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/SecureChannel.java
+++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/context/SecureChannel.java
@@ -740,6 +740,18 @@ public class SecureChannel {
PascalString policyId = selectedEndpoint.getValue().getPolicyId();
UserTokenType tokenType = selectedEndpoint.getValue().getTokenType();
SecurityPolicy tokenSecurityPolicy = userTokenSecurityPolicy(selectedEndpoint);
+ if (tokenType == UserTokenType.userTokenTypeUserName
+ && tokenSecurityPolicy == SecurityPolicy.NONE) {
+ // Nothing signs or encrypts this, so the password goes out in the clear where anything
+ // on the path can read it - and unlike data, a password read once is useful forever.
+ if (!configuration.isAllowInsecureCredentials()) {
+ throw new PlcRuntimeException("Refusing to send the username and password over a "
+ + "channel that neither signs nor encrypts. Configure a security-policy that "
+ + "does, or set allow-insecure-credentials=true to send them anyway.");
+ }
+ LOGGER.warn("allow-insecure-credentials is set: sending the username and password over "
+ + "a channel that neither signs nor encrypts");
+ }
ExtensionObject userIdentityToken = getIdentityToken(tokenType, policyId.getStringValue(), tokenSecurityPolicy);
RequestHeader requestHeader = conversation.createRequestHeader();
diff --git a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/OpcuaPlcDriverTest.java b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/OpcuaPlcDriverTest.java
index fb8a34d3aa..7c77a225dc 100644
--- a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/OpcuaPlcDriverTest.java
+++ b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/OpcuaPlcDriverTest.java
@@ -228,6 +228,7 @@ public class OpcuaPlcDriverTest {
private final String discoveryCorruptedParamWrongName = "diskovery=false";
private String tcpConnectionAddress;
+ private String untrustedTcpConnectionAddress;
private List<String> connectionStringValidSet;
final List<String> discoveryParamValidSet = List.of(discoveryValidParamTrue, discoveryValidParamFalse);
@@ -235,7 +236,23 @@ public class OpcuaPlcDriverTest {
@BeforeEach
public void startUp() throws Exception {
- tcpConnectionAddress = String.format(opcPattern + miloLocalAddress, milo.getHost(), milo.getMappedPort(12686)) + "?endpoint-port=12686";
+ // The test server is a container with a self-signed certificate and no anchor we could
+ // trust, so these connections say so. The driver's default policy signs and encrypts, and
+ // that needs the server's certificate to be trusted one way or another.
+ // The driver's default policy signs and encrypts, which needs both a client key pair to
+ // sign with and a way to trust the server's certificate. The test server is a container
+ // with a self-signed certificate, hence pinning it rather than a trust store.
+ //
+ // Kept separate from the address itself, because a test that asserts what happens without
+ // a trust anchor has to be able to leave the anchor out.
+ untrustedTcpConnectionAddress =
+ String.format(opcPattern + miloLocalAddress, milo.getHost(), milo.getMappedPort(12686))
+ + "?endpoint-port=12686"
+ + "&key-store-file=" + CLIENT_KEY_STORE.getAbsoluteFile().toString().replace("\\", "/")
+ + "&key-store-password=changeit"
+ + "&key-store-type=pkcs12";
+ tcpConnectionAddress = untrustedTcpConnectionAddress
+ + "&server-certificate-file=" + SERVER_CERTIFICATE.toString().replace("\\", "/");
connectionStringValidSet = List.of(tcpConnectionAddress);
}
@@ -941,7 +958,8 @@ public class OpcuaPlcDriverTest {
entry("message-security", MessageSecurity.SIGN.name())
);
assertThatThrownBy(() ->
- new DefaultPlcDriverManager().getConnection(tcpConnectionAddress + PARAM_DIVIDER + options))
+ new DefaultPlcDriverManager().getConnection(
+ untrustedTcpConnectionAddress + PARAM_DIVIDER + options))
.isInstanceOf(Exception.class);
}
diff --git a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/context/SecureChannelUserIdentityTest.java b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/context/SecureChannelUserIdentityTest.java
index 7f577b5621..725fdfca5a 100644
--- a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/context/SecureChannelUserIdentityTest.java
+++ b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/context/SecureChannelUserIdentityTest.java
@@ -213,6 +213,9 @@ class SecureChannelUserIdentityTest {
*/
@Test
void leavesUsernameAuthenticationUntouched() throws Exception {
+ // This covers how the token is built, not whether sending it is wise: the endpoint below
+ // protects nothing, which the driver otherwise refuses for a password.
+ configuration.setAllowInsecureCredentials(true);
SecureChannel channel = new SecureChannel(conversation, driverContext, configuration,
new PlcUsernamePasswordAuthentication("user", "secret"));
@@ -224,6 +227,36 @@ class SecureChannelUserIdentityTest {
assertEquals(request.getClientSignature(), request.getUserTokenSignature());
}
+ /**
+ * A password sent over a channel that neither signs nor encrypts is readable by anything on the
+ * path, and unlike a value it stays useful long after it was read.
+ */
+ @Test
+ void refusesToSendAPasswordOverAnUnprotectedChannel() {
+ SecureChannel channel = new SecureChannel(conversation, driverContext, configuration,
+ new PlcUsernamePasswordAuthentication("user", "secret"));
+
+ PlcRuntimeException thrown = assertThrows(PlcRuntimeException.class,
+ () -> channel.createActivateSessionRequest(usernameEndpoint()));
+ assertTrue(thrown.getMessage().contains("allow-insecure-credentials"),
+ "the refusal should name the way out, but was: " + thrown.getMessage());
+ }
+
+ /**
+ * The way out has to work, or the message above is telling operators to do something that does
+ * not help them.
+ */
+ @Test
+ void sendsThePasswordAnywayWhenExplicitlyAllowed() throws Exception {
+ configuration.setAllowInsecureCredentials(true);
+ SecureChannel channel = new SecureChannel(conversation, driverContext, configuration,
+ new PlcUsernamePasswordAuthentication("user", "secret"));
+
+ assertInstanceOf(org.apache.plc4x.java.opcua.readwrite.UserNameIdentityToken.class,
+ ((BinaryExtensionObjectWithMask) channel.createActivateSessionRequest(usernameEndpoint())
+ .getUserIdentityToken()).getBody());
+ }
+
private PlcCertificateAuthentication certificateAuthentication(String alias) throws Exception {
return new PlcCertificateAuthentication(keyStoreWith(entry("operator", user)), PASSWORD, alias);
}