OPC UA 安全通道与证书信任链配置实战

OPC UA 安全通道配置的第一步不是改代码,而是先把证书信任链建起来。绝大多数暴露在 4840 端口的 OPC UA 服务端出厂默认同时接受 None 策略与匿名用户令牌,扫描器一个握手就能读到全部节点树,甚至直接写变量。本文以可复现的顺序,把内部 CA、应用实例证书、SecurityPolicy 收敛和用户令牌策略一次配到位,并给出不用装商业客户端也能验证的方法。

适用场景

  • 工厂或园区内的 OPC UA 服务端(KEPServerEX、Ignition、open62541、Prosys 等)对办公网或跨网段可达。
  • 等保或工控安全检查要求「工业协议必须启用加密与身份认证,禁止匿名访问」。
  • 上位机、SCADA 与 MES 之间需要双向校验,避免中间人劫持变量写入。
  • 曾出现 4840 被外部 IP 直连探测、或服务端日志出现大量匿名 Session 的情况。

前置条件

  • OPC UA 服务端支持 Basic256Sha256 或 Aes256Sha256RsaPss 策略(OPC UA 1.04 及以上规范客户端普遍支持前者)。
  • 一台可签发证书的机器(openssl 1.1.1+),并为每台设备准备一个唯一应用名,例如 urn:example:server:plc-line1。
  • 服务端与客户端时钟已同步(NTP 偏差建议小于 5 分钟)。
  • 可修改服务端配置文件或通过其管理界面操作,且有重启窗口。
  • 测试用 Python 环境可安装 asyncua(仅用于验证,不影响生产)。

原理说明

OPC UA 的安全模型有三个彼此独立的开关,容易被混为一谈。其一是 SecurityPolicy:决定用哪套算法做签名与加密,例如 Basic256Sha256;与之搭配的 MessageSecurityMode 决定强度,None 为明文、Sign 为只签名不加密、SignAndEncrypt 才是签名加密都做。其二是 应用实例证书:每个客户端与服务端各有一张 X.509 证书,证书里的 ApplicationUri(SAN 中的 URI)必须与端点声明的 URI 完全一致,否则对方会拒绝连接。其三是 用户令牌策略:决定谁能登录,匿名、用户名口令或证书,与传输加密无关。

三者常被误配成「加密了但能匿名登录」或「有证书却用 None 策略」。正确组合是:SignAndEncrypt + Basic256Sha256 及以上 + 关闭匿名 + 用户名口令或客户端证书。此外,双方必须把对方证书放进各自的信任列表(trusted),把过期与吊销的证书放进拒绝列表(revoked),信任关系是双向的,只配一侧不会成功。

操作步骤

第一步:搭建内部 CA 并签发应用实例证书

先建一条只用于 OPC UA 的私有 CA,再为服务端与客户端分别签发证书。extendedKeyUsage 必须同时包含 serverAuth 与 clientAuth,否则会在角色校验环节被拒:

# 1) 生成 CA 私钥与自签根证书
openssl genrsa -out ua-ca.key 4096
openssl req -x509 -new -nodes -key ua-ca.key -sha256 -days 3650 \
  -subj "/C=CN/O=Example/CN=Example OPC UA CA" \
  -out ua-ca.crt

# 2) 服务端证书:ApplicationUri 必须是 urn 形式
openssl genrsa -out ua-server.key 2048
openssl req -new -key ua-server.key \
  -subj "/C=CN/O=Example/CN=plc-line1" \
  -out ua-server.csr

cat > ua-server.ext <<'EOF'
basicConstraints=CA:FALSE
keyUsage=digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment
extendedKeyUsage=serverAuth,clientAuth
subjectAltName=URI:urn:example:server:plc-line1,DNS:plc-line1.plant.local,IP:10.30.1.21
EOF

openssl x509 -req -in ua-server.csr -CA ua-ca.crt -CAkey ua-ca.key \
  -CAcreateserial -out ua-server.crt -days 825 -sha256 -extfile ua-server.ext

客户端证书同理,只把 ApplicationUri 换成 urn:example:client:hmi-line1、CN 换成 hmi-line1、SAN 中的 IP 换成客户端地址。私钥权限一律收到 600,并且只给运行服务的账号可读。

第二步:服务端收敛安全策略与用户令牌

以下为 open62541 的配置要点,其他产品在管理界面里能找到同名选项,含义一致。关键是 不要保留 None:

UA_ServerConfig *config = UA_ServerConfig_new_default();

/* 加载应用实例证书与私钥 */
UA_ByteString certificate = loadFile("ua-server.crt");
UA_ByteString privateKey  = loadFile("ua-server.key");
UA_ServerConfig_setDefaultWithSecurityPolicies(
    config, 4840,
    &certificate, &privateKey,
    NULL, 0,          /* 信任列表:由下面的目录扫描加载 */
    NULL, 0, 0);

/* 端点只保留 Basic256Sha256 + SignAndEncrypt */
UA_ServerConfig_removeSecurityPolicy(config,
    "http://opcfoundation.org/UA/SecurityPolicy#None");

/* 关闭匿名令牌,只允许用户名口令 */
config->endpoints[0].securityMode = UA_MESSAGESECURITYMODE_SIGNANDENCRYPT;
config->accessControl = UA_AccessControl_default(
    config, UA_FALSE, &config->securityPolicies[0].policyUri, 2, usernamePasswords);

KEPServerEX 或 Ignition 这类图形化产品,对应操作是:打开 OPC UA 配置 → 运行时 → 去掉 None 与 Basic128Rsa15 勾选,只留 Basic256Sha256 起步的策略;把安全模式设为 Sign & Encrypt;在用户管理器里删除 Anonymous 用户,为每台 HMI 建独立账号并设置强口令。

第三步:建立双向信任列表

OPC UA 的信任是目录式的:把对方证书文件按 DER 格式放入 trusted 目录,拒绝的放进 revoked。目录结构与权限如下:

# 目录结构(服务端)
/opt/opcua/pki/
├── own/certs/ua-server.der
├── own/private/ua-server.key      # 600,仅服务账号可读
├── trusted/certs/ua-client.der
├── revoked/certs/
└── issuers/certs/ua-ca.der

# PEM 转 DER,OPC UA 只认 DER
openssl x509 -in ua-server.crt -outform der -out /opt/opcua/pki/own/certs/ua-server.der
openssl x509 -in ua-client.crt -outform der -out /opt/opcua/pki/trusted/certs/ua-client.der
openssl x509 -in ua-ca.crt     -outform der -out /opt/opcua/pki/issuers/certs/ua-ca.der

chmod 600 /opt/opcua/pki/own/private/ua-server.key
chown -R opcua:opcua /opt/opcua/pki

客户端一侧要做同样的事:把自己的证书与私钥放 own,把服务端证书放 trusted。任何一侧漏放,握手都会以 BadSecurityChecksFailed 结束。

第四步:网络层只放行必要来源

证书只解决「谁能连」,端口收敛解决「从哪能连」。用 nftables 把 4840 限制到 HMI 与 SCADA 网段,其余直接丢弃:

nft add table inet ua
nft add chain inet ua input '{ type filter hook input priority 0 \; }'
nft add rule inet ua input tcp dport 4840 ip saddr 10.30.1.0/24 accept
nft add rule inet ua input tcp dport 4840 drop
nft list ruleset | grep -A5 'table inet ua'

配置验证

不需要装商业客户端,用 asyncua 就能确认策略是否真的收敛:

# pip install asyncua
import asyncio
from asyncua import Client, ua

async def main():
    url = "opc.tcp://plc-line1.plant.local:4840"
    # 1) 用 SignAndEncrypt 连接,应成功
    async with Client(url=url) as c:
        c.set_security_string(
            "Basic256Sha256,SignAndEncrypt,"
            "/opt/opcua/pki/own/certs/hmi-line1.der,"
            "/opt/opcua/pki/own/private/hmi-line1.key")
        print("server cert:", c.get_node(ua.NodeId(2258)).read_value())
    # 2) 用 None 策略连接,应被拒绝
    try:
        async with Client(url=url) as c2:
            print("unexpected: None policy accepted")
    except Exception as e:
        print("None rejected:", type(e).__name__)

asyncio.run(main())
  • 端点枚举:连接后遍历 get_endpoints(),返回列表中不应再出现 SecurityPolicyUri 以 None 结尾的条目。
  • 匿名登录:不传用户名口令直接 OpenSecureChannel + CreateSession,应返回 BadIdentityTokenInvalid 或 BadUserAccessDenied。
  • 证书链:openssl verify -CAfile ua-ca.crt ua-server.crt 应输出 OK;openssl x509 -in ua-server.crt -noout -text | grep -A2 "Subject Alternative Name" 确认 URI 与端点声明一致。
  • 流量侧:在网关抓包,SignAndEncrypt 下 Read 请求体应为密文,看不到节点 ID 明文。

常见问题(FAQ)

FAQ 1:证书配好了,客户端仍报 BadSecurityChecksFailed?

按顺序查三件事。第一,ApplicationUri 是否完全一致,包括 urn 前缀、大小写与末尾有没有多余斜杠,这个字段在服务端日志里通常能看到对方声明的值,直接对比最快。第二,证书格式,OPC UA 规范使用 DER,把 PEM 丢进 trusted 目录会被判定为无效证书。第三,时间,证书 notBefore 晚于当前时间或已过期都会失败,同时检查两端 NTP,时钟偏差过大同样触发校验失败。

FAQ 2:只开 Sign 不开 Encrypt 可以吗?

不建议。Sign 模式只保证报文未被篡改,节点 ID、变量值、用户名口令仍以明文在网络上传输,任何能镜像流量的位置都能还原工艺参数。工控现场的交换机镜像口、无线桥接、运维笔记本抓包都是现实中的泄露点。要么 SignAndEncrypt,要么不接入,中间态在合规检查里也过不了。

FAQ 3:证书有效期怎么定,换证会不会中断生产?

应用实例证书建议 825 天以内(与主流 TLS 生态一致),CA 可以长一些但要单独保管私钥。换证本身不中断连接,前提是先把新证书放进对方 trusted 目录再切换,旧证书保留一个维护窗口后再移走;直接覆盖会导致正在运行的会话在下一次重连时被拒。把到期时间写进监控,提前 30 天告警,比事后排查便宜得多。

总结

OPC UA 安全通道的落地顺序是固定的:先有内部 CA 与双向信任目录,再谈策略收敛;端点只留 Basic256Sha256 起的 SignAndEncrypt,删掉 None;删掉匿名用户令牌,按设备建账号;最后用 nftables 把 4840 收敛到 HMI 网段。四步互相独立,缺任何一步都留下可被利用的入口——只加密不禁匿名,等于给门上了锁却把钥匙插在锁上。