Active Directory compatibility. LdapOptions xml-doc expanded with a cheat-sheet covering Server (DC FQDN), Port 389 vs 636, UseTls=true under AD LDAP-signing enforcement, dedicated read-only service account DN, sAMAccountName vs userPrincipalName vs cn trade-offs, memberOf DN shape (CN=Group,OU=...,DC=... with the CN= RDN stripped to become the GroupToRole key), and the explicit 'nested groups NOT expanded' call-out (LDAP_MATCHING_RULE_IN_CHAIN / tokenGroups is a future authenticator enhancement, not a config change). docs/security.md §'Active Directory configuration' adds a complete appsettings.json snippet with realistic AD group names (OPCUA-Operators → WriteOperate, OPCUA-Engineers → WriteConfigure, OPCUA-AlarmAck → AlarmAck, OPCUA-Tuners → WriteTune), LDAPS port 636, TLS on, insecure-LDAP off, and operator-facing notes on each field. LdapUserAuthenticatorAdCompatTests (5 unit guards): ExtractFirstRdnValue parses AD-style 'CN=OPCUA-Operators,OU=...,DC=...' DNs correctly (case-preserving — operators' GroupToRole keys stay readable); also handles mixed case and spaces in group names ('Domain Users'); also works against the OpenLDAP ou=<group>,ou=groups shape (GLAuth) so one extractor tolerates both memberOf formats common in the field; EscapeLdapFilter escapes the RFC 4515 injection set (\, *, (, ), \0) so a malicious login like 'admin)(cn=*' can't break out of the filter; default UserNameAttribute regression guard.
Test posture — Server.Tests Unit: 43 pass / 0 fail (38 prior + 5 new AD-compat guards). Server.Tests LiveLdap category: 6 pass / 0 fail against running GLAuth (would skip cleanly without). Server build clean, 0 errors, 0 warnings.
Deferred: the session-identity end-to-end check (drive a full OPC UA UserName session, then read a 'whoami' node to verify the role landed on RoleBasedIdentity). That needs a test-only address-space node and is scoped for a separate PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
394 lines
20 KiB
Markdown
394 lines
20 KiB
Markdown
# Transport Security
|
|
|
|
## Overview
|
|
|
|
The LmxOpcUa server supports configurable transport security profiles that control how data is protected on the wire between OPC UA clients and the server.
|
|
|
|
There are two distinct layers of security in OPC UA:
|
|
|
|
- **Transport security** -- secures the communication channel itself using TLS-style certificate exchange, message signing, and encryption. This is what the `Security` configuration section controls.
|
|
- **UserName token encryption** -- protects user credentials (username/password) sent during session activation. The OPC UA stack encrypts UserName tokens using the server's application certificate regardless of the transport security mode. This means UserName authentication works on `None` endpoints too — the credentials themselves are always encrypted. However, a secure transport profile adds protection against message-level tampering and eavesdropping of data payloads.
|
|
|
|
## Supported Security Profiles
|
|
|
|
The server supports seven transport security profiles:
|
|
|
|
| Profile Name | Security Policy | Message Security Mode | Description |
|
|
|-----------------------------------|----------------------------|-----------------------|--------------------------------------------------|
|
|
| `None` | None | None | No signing or encryption. Suitable for development and isolated networks only. |
|
|
| `Basic256Sha256-Sign` | Basic256Sha256 | Sign | Messages are signed but not encrypted. Protects against tampering but data is visible on the wire. |
|
|
| `Basic256Sha256-SignAndEncrypt` | Basic256Sha256 | SignAndEncrypt | Messages are both signed and encrypted. Full protection against tampering and eavesdropping. |
|
|
| `Aes128_Sha256_RsaOaep-Sign` | Aes128_Sha256_RsaOaep | Sign | Modern profile with AES-128 encryption and SHA-256 signing. |
|
|
| `Aes128_Sha256_RsaOaep-SignAndEncrypt` | Aes128_Sha256_RsaOaep | SignAndEncrypt | Modern profile with AES-128 encryption. Recommended for production. |
|
|
| `Aes256_Sha256_RsaPss-Sign` | Aes256_Sha256_RsaPss | Sign | Strongest profile with AES-256 and RSA-PSS signatures. |
|
|
| `Aes256_Sha256_RsaPss-SignAndEncrypt` | Aes256_Sha256_RsaPss | SignAndEncrypt | Strongest profile. Recommended for high-security deployments. |
|
|
|
|
Multiple profiles can be enabled simultaneously. The server exposes a separate endpoint for each configured profile, and clients select the one they prefer during connection.
|
|
|
|
If no valid profiles are configured (or all names are unrecognized), the server falls back to `None` with a warning in the log.
|
|
|
|
## Configuration
|
|
|
|
Transport security is configured in the `Security` section of `appsettings.json`:
|
|
|
|
```json
|
|
{
|
|
"Security": {
|
|
"Profiles": ["None"],
|
|
"AutoAcceptClientCertificates": true,
|
|
"RejectSHA1Certificates": true,
|
|
"MinimumCertificateKeySize": 2048,
|
|
"PkiRootPath": null,
|
|
"CertificateSubject": null
|
|
}
|
|
}
|
|
```
|
|
|
|
### Properties
|
|
|
|
| Property | Type | Default | Description |
|
|
|--------------------------------|------------|--------------------------------------------------|-------------|
|
|
| `Profiles` | `string[]` | `["None"]` | List of security profile names to expose as server endpoints. Valid values: `None`, `Basic256Sha256-Sign`, `Basic256Sha256-SignAndEncrypt`, `Aes128_Sha256_RsaOaep-Sign`, `Aes128_Sha256_RsaOaep-SignAndEncrypt`, `Aes256_Sha256_RsaPss-Sign`, `Aes256_Sha256_RsaPss-SignAndEncrypt`. Profile names are case-insensitive. Duplicates are ignored. |
|
|
| `AutoAcceptClientCertificates` | `bool` | `true` | When `true`, the server automatically trusts client certificates that are not already in the trusted store. Set to `false` in production for explicit trust management. |
|
|
| `RejectSHA1Certificates` | `bool` | `true` | When `true`, client certificates signed with SHA-1 are rejected. SHA-1 is considered cryptographically weak. |
|
|
| `MinimumCertificateKeySize` | `int` | `2048` | Minimum RSA key size (in bits) required for client certificates. Certificates with shorter keys are rejected. |
|
|
| `PkiRootPath` | `string?` | `null` (defaults to `%LOCALAPPDATA%\OPC Foundation\pki`) | Override for the PKI root directory where certificates are stored. When `null`, uses the OPC Foundation default location. |
|
|
| `CertificateSubject` | `string?` | `null` (defaults to `CN={ServerName}, O=ZB MOM, DC=localhost`) | Override for the server certificate subject name. When `null`, the subject is derived from the configured `ServerName`. |
|
|
|
|
### Example: Development (no security)
|
|
|
|
```json
|
|
{
|
|
"Security": {
|
|
"Profiles": ["None"],
|
|
"AutoAcceptClientCertificates": true
|
|
}
|
|
}
|
|
```
|
|
|
|
### Example: Production (encrypted only)
|
|
|
|
```json
|
|
{
|
|
"Security": {
|
|
"Profiles": ["Basic256Sha256-SignAndEncrypt"],
|
|
"AutoAcceptClientCertificates": false,
|
|
"RejectSHA1Certificates": true,
|
|
"MinimumCertificateKeySize": 2048
|
|
}
|
|
}
|
|
```
|
|
|
|
### Example: Mixed (sign and encrypt endpoints, no plaintext)
|
|
|
|
```json
|
|
{
|
|
"Security": {
|
|
"Profiles": ["Basic256Sha256-Sign", "Basic256Sha256-SignAndEncrypt"],
|
|
"AutoAcceptClientCertificates": false
|
|
}
|
|
}
|
|
```
|
|
|
|
## PKI Directory Layout
|
|
|
|
The server stores certificates in a directory-based PKI store. The default root is:
|
|
|
|
```
|
|
%LOCALAPPDATA%\OPC Foundation\pki\
|
|
```
|
|
|
|
This can be overridden with the `PkiRootPath` setting. The directory structure is:
|
|
|
|
```
|
|
pki/
|
|
own/ Server's own application certificate and private key
|
|
issuer/ CA certificates that issued trusted client certificates
|
|
trusted/ Explicitly trusted client (peer) certificates
|
|
rejected/ Certificates that were presented but not trusted
|
|
```
|
|
|
|
### Certificate Trust Flow
|
|
|
|
When a client connects using a secure profile (`Sign` or `SignAndEncrypt`), the following trust evaluation occurs:
|
|
|
|
1. The client presents its application certificate during the secure channel handshake.
|
|
2. The server checks whether the certificate exists in the `trusted/` store.
|
|
3. If found, the connection proceeds (subject to key size and SHA-1 checks).
|
|
4. If not found and `AutoAcceptClientCertificates` is `true`, the certificate is automatically copied to `trusted/` and the connection proceeds.
|
|
5. If not found and `AutoAcceptClientCertificates` is `false`, the certificate is copied to `rejected/` and the connection is refused.
|
|
6. Regardless of trust status, the certificate must meet the `MinimumCertificateKeySize` requirement and pass the SHA-1 check (if `RejectSHA1Certificates` is `true`).
|
|
|
|
On first startup with a secure profile, the server automatically generates a self-signed application certificate in the `own/` directory if one does not already exist.
|
|
|
|
## Production Hardening
|
|
|
|
The default settings prioritize ease of development. Before deploying to production, apply the following changes:
|
|
|
|
### 1. Disable automatic certificate acceptance
|
|
|
|
Set `AutoAcceptClientCertificates` to `false` so that only explicitly trusted client certificates are accepted:
|
|
|
|
```json
|
|
{
|
|
"Security": {
|
|
"AutoAcceptClientCertificates": false
|
|
}
|
|
}
|
|
```
|
|
|
|
After changing this setting, you must manually copy each client's application certificate (the `.der` file) into the `trusted/` directory.
|
|
|
|
### 2. Remove the None profile
|
|
|
|
Remove `None` from the `Profiles` list to prevent unencrypted connections:
|
|
|
|
```json
|
|
{
|
|
"Security": {
|
|
"Profiles": ["Aes256_Sha256_RsaPss-SignAndEncrypt"]
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Configure LDAP authentication
|
|
|
|
Enable LDAP authentication to validate credentials against the GLAuth server. LDAP group membership controls what each user can do (read, write, alarm acknowledgment). See [Configuration Guide](Configuration.md) for the full LDAP property reference.
|
|
|
|
```json
|
|
{
|
|
"Authentication": {
|
|
"AllowAnonymous": false,
|
|
"AnonymousCanWrite": false,
|
|
"Ldap": {
|
|
"Enabled": true,
|
|
"Host": "localhost",
|
|
"Port": 3893,
|
|
"BaseDN": "dc=lmxopcua,dc=local",
|
|
"ServiceAccountDn": "cn=serviceaccount,dc=lmxopcua,dc=local",
|
|
"ServiceAccountPassword": "serviceaccount123"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
While UserName tokens are always encrypted by the OPC UA stack (using the server certificate), enabling a secure transport profile adds protection against message-level tampering and data eavesdropping.
|
|
|
|
### 4. Review the rejected certificate store
|
|
|
|
Periodically inspect the `rejected/` directory. Certificates that appear here were presented by clients but were not trusted. If you recognize a legitimate client certificate, move it to the `trusted/` directory to grant access.
|
|
|
|
## X.509 Certificate Authentication
|
|
|
|
The server supports X.509 certificate-based user authentication in addition to Anonymous and UserName tokens. When any non-None security profile is configured, the server advertises `UserTokenType.Certificate` in its endpoint descriptions.
|
|
|
|
Clients can authenticate by presenting an X.509 certificate. The server extracts the Common Name (CN) from the certificate subject and assigns the `AuthenticatedUser` and `ReadOnly` roles. The authentication is logged with the certificate's CN, subject, and thumbprint.
|
|
|
|
X.509 authentication is available automatically when transport security is enabled -- no additional configuration is required.
|
|
|
|
## Audit Logging
|
|
|
|
The server generates audit log entries for security-relevant operations. All audit entries use the `AUDIT:` prefix and are written to the Serilog rolling file sink for compliance review.
|
|
|
|
Audited events:
|
|
- **Authentication success**: Logs username, assigned roles, and session ID
|
|
- **Authentication failure**: Logs username and session ID
|
|
- **X.509 authentication**: Logs certificate CN, subject, and thumbprint
|
|
- **Certificate validation**: Logs certificate subject, thumbprint, and expiry for all validation events (accepted or rejected)
|
|
- **Write access denial**: Logged by the role-based access control system when a user lacks the required role
|
|
|
|
Example audit log entries:
|
|
```
|
|
AUDIT: Authentication SUCCESS for user admin with roles [ReadOnly, WriteOperate, AlarmAck] session abc123
|
|
AUDIT: Authentication FAILED for user baduser from session def456
|
|
X509 certificate authenticated: CN=ClientApp, Subject=CN=ClientApp,O=Acme, Thumbprint=AB12CD34
|
|
```
|
|
|
|
## CLI Examples
|
|
|
|
The Client CLI supports the `-S` (or `--security`) flag to select the transport security mode when connecting. Valid values are `none`, `sign`, `encrypt`, and `signandencrypt`.
|
|
|
|
### Connect with no security
|
|
|
|
```bash
|
|
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840/LmxOpcUa -S none
|
|
```
|
|
|
|
### Connect with signing
|
|
|
|
```bash
|
|
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840/LmxOpcUa -S sign
|
|
```
|
|
|
|
### Connect with signing and encryption
|
|
|
|
```bash
|
|
dotnet run --project src/ZB.MOM.WW.OtOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840/LmxOpcUa -S encrypt
|
|
```
|
|
|
|
### Browse with encryption and authentication
|
|
|
|
```bash
|
|
dotnet run -- browse -u opc.tcp://localhost:4840/LmxOpcUa -S encrypt -U operator -P secure-password -r -d 3
|
|
```
|
|
|
|
### Read a node with signing
|
|
|
|
```bash
|
|
dotnet run -- read -u opc.tcp://localhost:4840/LmxOpcUa -S sign -n "ns=2;s=TestMachine_001/Speed"
|
|
```
|
|
|
|
The CLI tool auto-generates its own client certificate on first use (stored under `%LOCALAPPDATA%\OpcUaCli\pki\own\`). When connecting to a server with `AutoAcceptClientCertificates` set to `false`, you must copy the CLI tool's certificate into the server's `trusted/` directory before the connection will succeed.
|
|
|
|
## Troubleshooting
|
|
|
|
### Certificate trust failure
|
|
|
|
**Symptom:** The client receives a `BadSecurityChecksFailed` or `BadCertificateUntrusted` error when connecting.
|
|
|
|
**Cause:** The server does not trust the client's certificate (or vice versa), and `AutoAcceptClientCertificates` is `false`.
|
|
|
|
**Resolution:**
|
|
1. Check the server's `rejected/` directory for the client's certificate file.
|
|
2. Copy the `.der` file from `rejected/` to `trusted/`.
|
|
3. Retry the connection.
|
|
4. If the server's own certificate is not trusted by the client, copy the server's certificate from `pki/own/certs/` to the client's trusted store.
|
|
|
|
### Endpoint mismatch
|
|
|
|
**Symptom:** The client receives a `BadSecurityModeRejected` or `BadSecurityPolicyRejected` error, or reports "No endpoint found with security mode...".
|
|
|
|
**Cause:** The client is requesting a security mode that the server does not expose. For example, the client requests `SignAndEncrypt` but the server only has `None` configured.
|
|
|
|
**Resolution:**
|
|
1. Verify the server's configured `Profiles` in `appsettings.json`.
|
|
2. Ensure the profile matching the client's requested mode is listed (e.g., add `Basic256Sha256-SignAndEncrypt` for encrypted connections).
|
|
3. Restart the server after changing the configuration.
|
|
4. Use the CLI tool to verify available endpoints:
|
|
```bash
|
|
dotnet run -- connect -u opc.tcp://localhost:4840/LmxOpcUa -S none
|
|
```
|
|
The output displays the security mode and policy of the connected endpoint.
|
|
|
|
### Server certificate not generated
|
|
|
|
**Symptom:** The server logs a warning about application certificate check failure on startup.
|
|
|
|
**Cause:** The `pki/own/` directory may not be writable, or the certificate generation failed.
|
|
|
|
**Resolution:**
|
|
1. Ensure the service account has write access to the PKI root directory.
|
|
2. Check that the `PkiRootPath` (if overridden) points to a valid, writable location.
|
|
3. Delete any corrupt certificate files in `pki/own/` and restart the server to trigger regeneration.
|
|
|
|
### SHA-1 certificate rejection
|
|
|
|
**Symptom:** A client with a valid certificate is rejected, and the server logs mention SHA-1.
|
|
|
|
**Cause:** The client's certificate was signed with SHA-1, and `RejectSHA1Certificates` is `true` (the default).
|
|
|
|
**Resolution:**
|
|
- Regenerate the client certificate using SHA-256 or stronger (recommended).
|
|
- Alternatively, set `RejectSHA1Certificates` to `false` in the server configuration (not recommended for production).
|
|
|
|
---
|
|
|
|
## LDAP Authentication
|
|
|
|
The server supports LDAP-based user authentication via GLAuth (or any standard LDAP server). When enabled, OPC UA `UserName` token credentials are validated by LDAP bind. LDAP group membership is resolved once during authentication and mapped to custom OPC UA role `NodeId`s in the `urn:zbmom:lmxopcua:roles` namespace. These role NodeIds are stored on the session's `RoleBasedIdentity.GrantedRoleIds` and checked directly during write and alarm-ack operations.
|
|
|
|
### Architecture
|
|
|
|
```
|
|
OPC UA Client → UserName Token → LmxOpcUa Server → LDAP Bind (validate credentials)
|
|
→ LDAP Search (resolve group membership)
|
|
→ Map groups to OPC UA role NodeIds
|
|
→ Store on RoleBasedIdentity.GrantedRoleIds
|
|
→ Permission checks via GrantedRoleIds.Contains()
|
|
```
|
|
|
|
### LDAP Groups and OPC UA Permissions
|
|
|
|
All authenticated LDAP users can browse and read nodes regardless of group membership. Groups grant additional permissions:
|
|
|
|
| LDAP Group | Permission |
|
|
|---|---|
|
|
| ReadOnly | No additional permissions (read-only access) |
|
|
| WriteOperate | Write FreeAccess and Operate attributes |
|
|
| WriteTune | Write Tune attributes |
|
|
| WriteConfigure | Write Configure attributes |
|
|
| AlarmAck | Acknowledge alarms |
|
|
|
|
Users can belong to multiple groups. The `admin` user in the default GLAuth configuration belongs to all groups.
|
|
|
|
### Effective Permission Matrix
|
|
|
|
The effective permission for a write operation depends on two factors: the user's session role (from LDAP group membership or anonymous access) and the Galaxy attribute's security classification. The security classification controls the node's `AccessLevel` — attributes classified as `SecuredWrite`, `VerifiedWrite`, or `ViewOnly` are exposed as read-only nodes regardless of the user's role. For writable classifications, the required write role depends on the classification.
|
|
|
|
| | FreeAccess | Operate | SecuredWrite | VerifiedWrite | Tune | Configure | ViewOnly |
|
|
|---|---|---|---|---|---|---|---|
|
|
| **Anonymous (`AnonymousCanWrite=true`)** | Write | Write | Read | Read | Write | Write | Read |
|
|
| **Anonymous (`AnonymousCanWrite=false`)** | Read | Read | Read | Read | Read | Read | Read |
|
|
| **ReadOnly** | Read | Read | Read | Read | Read | Read | Read |
|
|
| **WriteOperate** | Write | Write | Read | Read | Read | Read | Read |
|
|
| **WriteTune** | Read | Read | Read | Read | Write | Read | Read |
|
|
| **WriteConfigure** | Read | Read | Read | Read | Read | Write | Read |
|
|
| **AlarmAck** (only) | Read | Read | Read | Read | Read | Read | Read |
|
|
| **Admin** (all groups) | Write | Write | Read | Read | Write | Write | Read |
|
|
|
|
All roles can browse and read all nodes. The "Read" entries above mean the node is either read-only by classification or the user lacks the required write role. "Write" means the write is permitted by both the node's classification and the user's role.
|
|
|
|
Alarm acknowledgment is an independent permission controlled by the `AlarmAck` role and is not affected by security classification.
|
|
|
|
### GLAuth Setup
|
|
|
|
The project uses [GLAuth](https://github.com/glauth/glauth) v2.4.0 as the LDAP server, installed at `C:\publish\glauth\`. See `C:\publish\glauth\auth.md` for the complete user/group reference and service management commands.
|
|
|
|
### Configuration
|
|
|
|
Enable LDAP in `appsettings.json` under `Authentication.Ldap`. See [Configuration Guide](Configuration.md) for the full property reference.
|
|
|
|
### Active Directory configuration
|
|
|
|
Production deployments typically point at Active Directory instead of GLAuth. Only four properties differ from the dev defaults: `Server`, `Port`, `UserNameAttribute`, and `ServiceAccountDn`. The same `GroupToRole` mechanism works — map your AD security groups to OPC UA roles.
|
|
|
|
```json
|
|
{
|
|
"OpcUaServer": {
|
|
"Ldap": {
|
|
"Enabled": true,
|
|
"Server": "dc01.corp.example.com",
|
|
"Port": 636,
|
|
"UseTls": true,
|
|
"AllowInsecureLdap": false,
|
|
"SearchBase": "DC=corp,DC=example,DC=com",
|
|
"ServiceAccountDn": "CN=OpcUaSvc,OU=Service Accounts,DC=corp,DC=example,DC=com",
|
|
"ServiceAccountPassword": "<from your secret store>",
|
|
"DisplayNameAttribute": "displayName",
|
|
"GroupAttribute": "memberOf",
|
|
"UserNameAttribute": "sAMAccountName",
|
|
"GroupToRole": {
|
|
"OPCUA-Operators": "WriteOperate",
|
|
"OPCUA-Engineers": "WriteConfigure",
|
|
"OPCUA-AlarmAck": "AlarmAck",
|
|
"OPCUA-Tuners": "WriteTune"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Notes:
|
|
|
|
- `UserNameAttribute: "sAMAccountName"` is the critical AD override — the default `uid` is not populated on AD user entries, so the user-DN lookup returns no results without it. Use `userPrincipalName` instead if operators log in with `user@corp.example.com` form.
|
|
- `Port: 636` + `UseTls: true` is required under AD's LDAP-signing enforcement. AD increasingly rejects plain-LDAP bind; set `AllowInsecureLdap: false` to refuse fallback.
|
|
- `ServiceAccountDn` should name a dedicated read-only service principal — not a privileged admin. The account needs read access to user and group entries in the search base.
|
|
- `memberOf` values come back as full DNs like `CN=OPCUA-Operators,OU=OPC UA Security Groups,OU=Groups,DC=corp,DC=example,DC=com`. The authenticator strips the leading `CN=` RDN value so operators configure `GroupToRole` with readable group common-names.
|
|
- Nested group membership is **not** expanded — assign users directly to the role-mapped groups, or pre-flatten membership in AD. `LDAP_MATCHING_RULE_IN_CHAIN` / `tokenGroups` expansion is an authenticator enhancement, not a config change.
|
|
|
|
### Security Considerations
|
|
|
|
- LDAP credentials are transmitted in plaintext over the OPC UA channel unless transport security is enabled. Use `Basic256Sha256-SignAndEncrypt` for production deployments.
|
|
- The GLAuth LDAP server itself listens on plain LDAP (port 3893). Enable LDAPS in `glauth.cfg` for environments where LDAP traffic crosses network boundaries.
|
|
- The service account password is stored in `appsettings.json`. Protect this file with appropriate filesystem permissions.
|