化工DCS系统安全防护实战:操作员双因素认证到PLC控制器全链路
某化工企业的中控室里出现过这样一幕:装置负荷异常波动,班长想调出「昨晚十点到十一点,谁给这条管线的调节阀下过指令」,翻了半小时日志只得到一个结果——四名操作员共用一个账号登录着三台操作站,离岗没有锁屏,日志里只有账号名,没有人的名字。再往深处查,工程师站的组态下装权限和日常开关阀混在同一批账号里,理论上任何一个人都能在下装组态的同时顺手改一个设定值。指令是真下了,但「谁下的」这件事,系统答不上来。
坐标先立:矿业化工线写到第 4 篇。前三篇分别立了装置内的密码改造(DCS 的 OT 特性如何约束实施方式)、数据出装置的监管上报链路、监测数据的分级保护;这一篇补上「人 → 控制器」这条线——操作员的身份如何从操作站出发,经上位机网关,一路带到 PLC 控制器面前。合规坐标是等保三级与 GB/T 39786 里反复出现的三个词:身份鉴别、访问控制、安全审计。控制器本身没有身份概念,这条链路的全部身份控制都要在人能触达的位置完成——这正是化工 DCS 身份落地与其他行业最大的不同。
01 | 控制指令链路的身份,难在哪儿
五条难点,每一条都直接指向「指令能不能追溯到人」:
- 追溯断在共用账号。化工装置连续生产,班组按班次轮换,很多企业索性按岗位建账号——一号多人、多人一号。平时省了账号管理的事,出了事日志只能追到账号,追不到人。身份问题在化工 DCS 里通常不是「没有认证」,而是「认证结果对不上具体的人」。
- PLC 侧没有身份概念。控制器执行的是报文,报文格式对了、校验和对了,它就执行。让控制器理解「这条指令是谁发的」在现有 PLC 上做不到,也不现实——改不了控制器固件,就只能在控制器之外的位置把身份做实,把不该进来的报文拦在前面。
- 工程师站与操作站权限不分。组态下装是控制链路上危险等级最高的动作:一次下装可以改变整段控制逻辑、改写报警限值。它和日常的开关阀、切手动自动混在一条通道、同一批账号里,等于把「改逻辑」和「用逻辑」放在了同一把权限下面。
- 可用性约束压着改造方式。装置不停车,身份改造不能要求停机重启,不能给控制链路引入单点故障,认证系统的故障不能演变成装置误停。OT 环境里身份方案是「加一层」而不是「换一层」。
- 防爆与井下场景的物理约束。装置区操作站的设备选型受防爆等级约束,井下更要考虑无网、粉尘、空间限制。硬件因子要选得进现场、用得长久,登录动作要快——操作员一手拿着对讲机一手操作,认证流程多一步都有人想办法绕过去。
这五条合起来,指向一个落地形态:身份在人这一层做实,指令在网关这一层把关,控制器只接受带签名与角色的报文。
02 | 机制拆解:从人到控制器的三段授权链
把整条链路拆成三段,每一段的风险、控制动作和留下的证据放进一张表:
| 段 | 现场风险 | 控制动作 | 留下的证据 |
|---|---|---|---|
| 操作站登录 | 共用账号、离岗未锁、单因子登录 | 一人一账号 + 双因子登录 + 离岗自动锁屏 | 登录日志(人/站/时间/因子) |
| 指令签发与验签 | 开度篡改、冒名签发、越权指令、旧指令重放 | 指令报文带操作员签名,上位机网关做验签、角色校验、时效检查 | 网关验签记录(通过/拒绝及原因) |
| 控制器入口 | 未授权主机直连控制器写数据 | 来源白名单:只有登记的上位机可以下发,其余一律拦截 | 直连拦截告警与处置记录 |
从人到控制器的三段授权链:
[操作员]──硬件因子+口令──▶ [操作站] ← 一人一账号/离岗锁屏/双因子
│ ① 会话令牌(带超时,超时须重新认证)
▼
[指令报文: 谁/何时/对哪个阀/开多少/类型]──SM2签名──▶
│ ② 上位机前置网关: 验签 → 角色校验 → 时效 → 来源白名单
▼
[上位机网关]──只放行登记主机──▶ [PLC 控制器] ← 控制器无身份概念,报文即一切
│ ③ 指令与结论整体签名落盘,审计到人
▼
[组态下装通道]──工程师+授权管理员双签名──▶ 变更版本与留痕落盘加密
为什么拦截点只能上移到上位机
PLC 的执行模型决定了它不适宜承担身份职责:它只认报文,不认人。有人会想到在控制器侧加白名单或访问控制列表,这能挡住一部分直连,但挡不住「用合法通道发非法指令」——指令格式合法、来源主机合法,内容却是越权的。所以身份控制必须前移到指令生成与转发的位置:操作站管「人是谁」,网关管「这条指令这个人能不能发」,控制器保持执行者角色。前移不是放弃控制器侧的防护,直连白名单仍然要留在网关出口上,两条腿缺一条都会留口子。
职责分离:组态下装走双人授权通道
实时操作与组态变更必须分成两条授权通道。日常操作由操作员单人签发即可,因为单条指令的影响面是一台执行机构;组态下装的影响面是整段控制逻辑,必须由工程师发起、授权管理员复核,两把签名齐备才放行——这就是双人授权。它防的不只是误操作,还有「一个人既能改逻辑又能删记录」这种结构性风险。检查里判断一家企业职责分离做没做,问一句「下装组态需要几个人」就能问出来:答案是「一个人就行」,后面所有权限模型都要重新对。
矿山侧的差异:井下与露天矿 SCADA 的身份落地
「矿山 SCADA 工控安全」和化工 DCS 是同一类问题、两套现场条件。井下作业面的网络条件差,无线覆盖不稳定,不能假设操作终端随时可达认证服务器,身份方案要容忍间歇连通——登录凭据校验要有本地兜底策略,恢复连通后补传审计记录;井下与露天矿的爆破器材库、提升机、主通风机这些点位防爆与安全等级更高,对应操作终端的设备形态受限,硬件因子要选工业级防护的产品;更重要的是可用性排序不同——化工装置怕误停,矿山场景里「井下通信中断时保生产指挥」常常排在更前面,所以矿山侧的身份策略要分点位定级:主通风机、提升机这类安全关键点位用最严的授权链,一般监测点位用简化链路,不能一刀切把最重的认证流程压到所有终端上。集控中心远程操作是矿山的常态,远程会话的身份证明与指令签名要绑定在一起,避免「登录是张三、操作是共享会话」的断链。
03 | 先跑通:控制指令授权链的六个环节
场景设定:某化工装置的操作员要给调节阀下发开度指令,指令要经过「操作站双因子登录 → 操作员签发 → 上位机网关验签与角色校验 → 组态下装双人授权 → 网关台账签名 → 台账防篡改」这条链路。这段演示把六件事跑通:(1)身份底账一人一档、操作员与工程师指令集互斥;(2)双因子登录、单因子拒收、离岗超时失效;(3)阀门指令验签放行,篡改、冒名、越权、直连四类异常全部拦截;(4)组态下装须工程师与授权管理员双签名;(5)网关台账签名密钥轮换后新旧记录均可验;(6)台账被替换但保留原签名时验签失败。下面用国密 SM2/SM3 复现。
# -*- coding: utf-8 -*-
# 化工 DCS 控制指令链路身份授权演示(国密 SM2/SM3/SM4)
# 场景:操作站双因子登录 → 操作员签发控制指令 → 上位机前置网关验签与角色校验 →
# 组态下装双人授权 → 指令台账留痕
# 边界:硬件令牌只作因子能力引用(机制前文已讲,此处不重复);
# PLC 侧没有身份概念,身份控制全部上移到操作站与上位机网关完成
import json
from gmssl import sm2, sm3, func
def sm3h(b: bytes) -> str:
return sm3.sm3_hash(func.bytes_to_list(b))
def digest(b: bytes) -> bytes:
return bytes.fromhex(sm3h(b))
def pub_of(p: str) -> str:
return sm2.CryptSM2(private_key=p, public_key="00")._kg(int(p, 16), sm2.default_ecc_table["g"])
def line(t: str, ok: bool):
print(f"[{t}] = {'True' if ok else 'False'}")
K = "20240601123456789abcdef0123456789abcdef0123456789abcdef012345678" # 固定K,仅演示用
# 四方密钥:操作员x2 / 工程师 / 授权管理员 / 冒名者;网关另有台账签名密钥对
OP_P = ("6a1b2c3d4e5f6071" "8293a4b5c6d7e8f9" "0a1b2c3d4e5f6071" "8293a4b5c6d7e8f1")
OP2_P = ("7b2c3d4e5f607182" "93a4b5c6d7e8f90a" "1b2c3d4e5f607182" "93a4b5c6d7e8f902")
ENG_P = ("8c3d4e5f60718293" "a4b5c6d7e8f90a1b" "2c3d4e5f60718293" "a4b5c6d7e8f90a03")
ADM_P = ("9d4e5f60718293a4" "b5c6d7e8f90a1b2c" "3d4e5f60718293a4" "b5c6d7e8f90a1b04")
ROG_P = ("ae5f60718293a4b5" "c6d7e8f90a1b2c3d" "4e5f60718293a4b5" "c6d7e8f90a1b2c05")
GW_P = ("bf60718293a4b5c6" "d7e8f90a1b2c3d4e" "5f60718293a4b5c6" "d7e8f90a1b2c3d06")
OP = sm2.CryptSM2(private_key=OP_P, public_key=pub_of(OP_P))
OP2 = sm2.CryptSM2(private_key=OP2_P, public_key=pub_of(OP2_P))
ENG = sm2.CryptSM2(private_key=ENG_P, public_key=pub_of(ENG_P))
ADM = sm2.CryptSM2(private_key=ADM_P, public_key=pub_of(ADM_P))
ROG = sm2.CryptSM2(private_key=ROG_P, public_key=pub_of(ROG_P))
GW = sm2.CryptSM2(private_key=GW_P, public_key=pub_of(GW_P))
# 身份底账:一人一档、一档一钥,角色决定可签发的指令类型
BOOK = {
"op-01": {"name": "操作员甲", "role": "operator", "allow": {"actuate"}},
"op-02": {"name": "操作员乙", "role": "operator", "allow": {"actuate"}},
"eng-01": {"name": "工程师丙", "role": "engineer", "allow": {"download"}},
"adm-01": {"name": "授权管理员丁", "role": "admin", "allow": {"download"}},
}
KEYS = {"op-01": OP, "op-02": OP2, "eng-01": ENG, "adm-01": ADM}
print("== 环节0:身份底账,一人一档与职责分离 ==")
op_allow = set().union(*(v["allow"] for v in BOOK.values() if v["role"] == "operator"))
eng_allow = set().union(*(v["allow"] for v in BOOK.values() if v["role"] == "engineer"))
line("账号一人一档、一档一钥,操作员与工程师可签发的指令集互斥",
len(BOOK) == len(KEYS) == len({v["name"] for v in BOOK.values()})
and not (op_allow & eng_allow))
print("== 环节1:操作站双因子登录与会话 ==")
NOW = 1789100000
def login(user: str, factors: set, now: int):
"""双因子登录:硬件因子与口令齐备才发会话令牌,单因子不发"""
if {"hardware", "pin"} <= factors:
return {"user": user, "exp": now + 300}
return None
def sess_ok(s, now: int) -> bool:
return s is not None and s["exp"] > now
sess = login("op-01", {"hardware", "pin"}, NOW)
line("硬件因子与口令双因子齐备,操作站登录通过", sess_ok(sess, NOW) and sess["user"] == "op-01")
line("仅口令单因子的登录请求被拒", not sess_ok(login("op-01", {"pin"}, NOW), NOW))
line("离岗超时后会话令牌失效,须重新认证", sess_ok(sess, NOW) and not sess_ok(sess, NOW + 600))
print("== 环节2:控制指令签发与前置网关把关 ==")
WHITELIST = {"op-station-1", "op-station-2", "eng-station-1"}
def gateway(cmd: dict, sig: str, host: str, now: int) -> bool:
"""上位机前置网关:验签 → 角色校验 → 时效 → 来源白名单,四道都过才放行"""
acct = BOOK.get(cmd.get("user", ""))
key = KEYS.get(cmd.get("user", ""))
if acct is None or key is None:
return False
raw = json.dumps({k: v for k, v in cmd.items() if k != "sig"},
sort_keys=True, ensure_ascii=False).encode()
if not key.verify(sig, digest(raw)): # 身份:签名对不上声明的账号即拒
return False
if cmd.get("type") not in acct["allow"]: # 角色:指令类型不在允许集即拒
return False
if cmd.get("ts", 0) < now - 30: # 时效:过期指令拒收,防重放
return False
if host not in WHITELIST: # 来源:未登记主机直连即拒
return False
return True
def issue(user: str, ctype: str, ts: int, **fields) -> dict:
cmd = {"user": user, "type": ctype, "ts": ts, **fields}
raw = json.dumps(cmd, sort_keys=True, ensure_ascii=False).encode()
cmd["sig"] = KEYS[user].sign(digest(raw), K)
return cmd
valve = issue("op-01", "actuate", NOW, tag="LCV-201", value=45)
line("操作员签发的阀门指令,网关验签与角色校验通过后下发", gateway(valve, valve["sig"], "op-station-1", NOW))
bad = {k: v for k, v in valve.items() if k != "sig"}
bad["value"] = 85
line("阀门开度被篡改(45→85)后原签名验签失败", not gateway(bad, valve["sig"], "op-station-1", NOW))
rogue_cmd = {"user": "op-01", "type": "actuate", "ts": NOW, "tag": "LCV-201", "value": 85}
rogue_raw = json.dumps(rogue_cmd, sort_keys=True, ensure_ascii=False).encode()
rogue_sig = ROG.sign(digest(rogue_raw), K)
rogue_cmd["sig"] = rogue_sig
line("冒名者冒用操作员账号签发的指令被拒", not gateway(rogue_cmd, rogue_sig, "op-station-1", NOW))
dl = issue("op-01", "download", NOW, pkg="cfg-v12")
line("操作员签发组态下装指令,签名真实但被角色校验拒绝", not gateway(dl, dl["sig"], "op-station-1", NOW))
line("未登记主机直连下发的写指令被网关拦截", not gateway(valve, valve["sig"], "laptop-unknown", NOW))
print("== 环节3:组态下装双人授权 ==")
def download_ok(pkg: str, ts: int, s_eng: str, s_adm: str) -> bool:
raw = json.dumps({"type": "download", "pkg": pkg, "ts": ts},
sort_keys=True, ensure_ascii=False).encode()
return (ENG.verify(s_eng, digest(raw)) and ADM.verify(s_adm, digest(raw))
and "download" in BOOK["eng-01"]["allow"] and "download" in BOOK["adm-01"]["allow"])
pkg_req = json.dumps({"type": "download", "pkg": "cfg-v12", "ts": NOW},
sort_keys=True, ensure_ascii=False).encode()
s_eng = ENG.sign(digest(pkg_req), K)
s_adm = ADM.sign(digest(pkg_req), K)
line("组态下装由工程师与授权管理员双签名齐备后放行", download_ok("cfg-v12", NOW, s_eng, s_adm))
line("缺少授权管理员签名的下装申请被拒", not download_ok("cfg-v12", NOW, s_eng, s_eng))
print("== 环节4:网关验签密钥轮换 ==")
GW_P_NEW = ("c0718293a4b5c6d7" "e8f90a1b2c3d4e5f" "60718293a4b5c6d7" "e8f90a1b2c3d4e07")
GW_NEW = sm2.CryptSM2(private_key=GW_P_NEW, public_key=pub_of(GW_P_NEW))
GWV_OLD = sm2.CryptSM2(private_key=None, public_key=pub_of(GW_P))
GWV_NEW = sm2.CryptSM2(private_key=None, public_key=pub_of(GW_P_NEW))
entry_old = json.dumps({"ts": NOW, "user": "op-01", "tag": "LCV-201", "value": 45},
sort_keys=True, ensure_ascii=False).encode()
entry_new = json.dumps({"ts": NOW + 60, "user": "op-02", "tag": "LCV-201", "value": 50},
sort_keys=True, ensure_ascii=False).encode()
sig_old = GW.sign(digest(entry_old), K)
sig_new = GW_NEW.sign(digest(entry_new), K)
line("网关台账签名密钥轮换后,新旧记录在重叠窗口内均可验",
GWV_NEW.verify(sig_new, digest(entry_new)) and GWV_OLD.verify(sig_old, digest(entry_old)))
print("== 环节5:指令台账留痕,审计到人 ==")
ledger = json.dumps({"ts": NOW, "user": "op-01", "type": "actuate", "tag": "LCV-201", "value": 45},
sort_keys=True, ensure_ascii=False).encode()
ledger_sig = GW.sign(digest(ledger), K)
line("指令台账整体签名,审计可追溯到具体操作员", GWV_OLD.verify(ledger_sig, digest(ledger)))
tampered = json.dumps({"ts": NOW, "user": "op-01", "type": "actuate", "tag": "LCV-201", "value": 0},
sort_keys=True, ensure_ascii=False).encode()
line("台账记录被替换(开度45→0)但保留原签名,验签失败", not GWV_OLD.verify(ledger_sig, digest(tampered)))
== 环节0:身份底账,一人一档与职责分离 ==
[账号一人一档、一档一钥,操作员与工程师可签发的指令集互斥] = True
== 环节1:操作站双因子登录与会话 ==
[硬件因子与口令双因子齐备,操作站登录通过] = True
[仅口令单因子的登录请求被拒] = True
[离岗超时后会话令牌失效,须重新认证] = True
== 环节2:控制指令签发与前置网关把关 ==
[操作员签发的阀门指令,网关验签与角色校验通过后下发] = True
[阀门开度被篡改(45→85)后原签名验签失败] = True
[冒名者冒用操作员账号签发的指令被拒] = True
[操作员签发组态下装指令,签名真实但被角色校验拒绝] = True
[未登记主机直连下发的写指令被网关拦截] = True
== 环节3:组态下装双人授权 ==
[组态下装由工程师与授权管理员双签名齐备后放行] = True
[缺少授权管理员签名的下装申请被拒] = True
== 环节4:网关验签密钥轮换 ==
[网关台账签名密钥轮换后,新旧记录在重叠窗口内均可验] = True
== 环节5:指令台账留痕,审计到人 ==
[指令台账整体签名,审计可追溯到具体操作员] = True
[台账记录被替换(开度45→0)但保留原签名,验签失败] = True
逐段注解:
- 环节0 是身份底账。一人一档、一档一钥是后面所有环节的前提:账号与人一一对应、每个账号绑定独立的签名密钥。操作员与工程师可签发的指令集互斥,职责分离从这里开始,而不是从权限配置界面开始。
- 环节1 是登录与会话。双因子齐备才发会话令牌,仅口令的登录请求被拒;会话带超时,离岗超时后旧令牌失效,须重新认证。这一条对应现场最常见的「离岗未锁」——超时策略把人的离岗从管理要求变成了技术强制。
- 环节2 是指令签发与网关把关。指令报文带操作员签名,网关按四道顺序检查:验签(身份)、角色校验(权限)、时效(防重放)、来源白名单(直连)。五个断言覆盖一条正常路径和四类异常——开度篡改、冒名签发、越权下装、未登记主机直连。特别注意「操作员签发组态下装指令」这一条:签名是真实的,验签能通过,但角色校验会拒——签名证明「是谁」,角色证明「能不能」,两件事缺一不可。
- 环节3 是组态下装双人授权。工程师发起、授权管理员复核,两把签名齐备才放行;缺任何一把都下不去。这条对应「一次下装改变整段控制逻辑」的高危性。
- 环节4 是台账签名密钥轮换。网关的台账签名密钥轮换后,轮换前后的记录在重叠窗口内都能验,事故追溯不因轮换断档。轮换窗口要可配置,这一点和监测数据那篇的分级轮换是同一个道理,但这里的对象是审计记录。
- 环节5 是台账防篡改。台账记录整体签名,审计可以追溯到具体操作员;把开度从 45 改成 0 再保留原签名,验签失败——改了内容就改不了签名。审计记录和业务数据用同一套完整性逻辑保护,检查时才能做到「记录即证据」。
验证点:现场演示时按「共用账号查不到人 → 双因子登录 → 指令验签与角色校验 → 越权下装被拒 → 台账验签」的顺序调出来。最有力的是两个画面:一是把开度从 45 改成 85 后验签失败——回应「指令怎么证明没被改过」;二是操作员签发下装指令被角色校验拒绝——回应「权限怎么证明分得开」。演示用固定随机数保证结果可复现;正式签发时随机数必须由安全模块内部生成,同一私钥配相同随机数会泄露私钥,这一点在演示之外必须守住。
04 | 落地动作:分三条线怎么做
控制指令链路的身份落地按三条线推进,每条线配一个可对照的整改样本。
线一:账号与角色治理(对应身份底账)
- 动作:把操作站、工程师站、上位机管理界面的账号盘一遍,清出共用账号、幽灵账号、离职未销账号;按「一人一账号、一账号一密钥」重建底账;画权限矩阵,把实时操作、组态下装、参数整定、审计查询四类动作按角色拆开,操作员与工程师的指令集互斥。
- 整改样本(背景→动作→结果):某化工企业四个班组共用两个操作站账号,日志追不到人 → 按班组花名册重建账号、绑定硬件因子,操作员与工程师分列两个角色域 → 一次工艺异常复盘时,十分钟内定位到具体的指令签发人和时间点。
- 注意节奏:账号重建要配合倒班表分批切换,不能一次全切——切换当晚就要有人盯操作站,防止有人因为「登录变慢」私建后门账号。
线二:指令链路加固(对应网关把关)
- 动作:在上位机部署前置网关,指令报文带操作员签名后经网关验签、角色校验、时效检查才下发控制器;网关出口挂来源白名单,未登记主机的写请求一律拦截并告警;操作站启用离岗锁屏与会话超时;登录双因子中硬件因子选工业级防护的产品,适配操作站现场条件。
- 整改样本:某装置曾发现笔记本接入控制网段后能直接读写 PLC 数据区 → 部署网关白名单后重测,非登记主机的写请求在网关处被拒并产生告警 → 渗透测试报告里「直连控制器」一项从高风险降为已处置。
- 注意边界:白名单是网关出口的防护,不能替代指令级的验签与角色校验——前者挡「谁在发」,后者挡「发的对不对」,两层一起才闭环。
线三:留痕与追溯(对应安全审计)
- 动作:指令报文与网关结论(通过/拒绝及原因)整体签名后落台账,四要素齐全(时间、账号、指令摘要、结论);组态下装记录单独成册,双签名、版本号、生效范围三样俱全;台账落盘加密,审计记录与业务数据同一套完整性逻辑;密钥轮换保留重叠窗口,历史台账在追溯期内始终可验。
- 整改样本:某企业原来只保留操作站本机日志,系统重装后日志丢失 → 改为网关集中落台账并整体签名加密 → 上级检查时出示的台账与网关记录逐条对得上,未再出现「日志各说各话」的情况。
三条线的推进顺序:先做账号与角色治理(线一),它是身份的底账,底账不清,后面两条都是空中楼阁;再做指令链路加固(线二),它把「人是谁」带进每一条指令;最后补留痕与追溯(线三),它决定出了事能不能说清楚。最常见的反向坑是先上认证产品再补账号治理——底账里还是共用账号,双因子认证绑在共享账号上,等于给一把公共钥匙加了把更贵的锁。
05 | 避坑清单:8 条最容易踩的坑
| # | 坑 | 后果 | 怎么验证避开了 |
|---|---|---|---|
| 1 | 操作站共用账号 | 日志到账号不到人,追溯断链 | 抽三个月登录日志,核对账号与人员排班表 |
| 2 | 离岗不锁屏、会话不过期 | 他人在已登录会话里冒用身份 | 现场离岗五分钟后回看是否已锁 |
| 3 | 工程师站与操作站同权限 | 日常操作者能改控制逻辑 | 用操作员账号尝试下发组态下装,应被拒 |
| 4 | 只做白名单不做指令验签 | 合法通道里的越权指令拦不住 | 篡改指令内容后重放,应验签失败 |
| 5 | 双因子绑在共享账号上 | 加了认证仍追不到人 | 核对认证凭据持有人与账号一一对应 |
| 6 | 下装无双人授权 | 一个人能改逻辑又能删记录 | 查最近三次下装记录是否各有两把签名 |
| 7 | 台账明文落盘、无整体签名 | 审计记录可被删改,举证失真 | 改一条台账记录,验签应失败 |
| 8 | 矿山侧一刀切上重认证 | 井下通信中断时指挥受阻 | 按点位分级核查:安全关键点位严、一般点位简 |
挑第 4 条展开:网关白名单是最容易被当成「做完」的防护,因为它见效直观——没登记的主机确实进不来。但它防的只是「谁在发」,防不了「发的对不对」。合法操作站的会话一旦被冒用(离岗未锁、令牌被盗用),攻击者手里的通道是完全合法的,此时能拦住篡改指令的只有指令报文上的操作员签名与网关的角色校验。两层的成本都不高:白名单是网关的配置项,指令签名是在既有报文格式上追加签名字段。只做前者等于锁了大门不锁抽屉。
06 | 合规视角:要对上哪些要求
- 等保三级(身份鉴别、访问控制、安全审计):三级系统要求对登录用户进行身份鉴别,宜采用双因素之一为密码技术;对重要主体设置访问控制策略;审计范围覆盖到每个用户、对重要事件可追溯。落到控制指令链路,就是「操作站双因子登录 + 指令按角色放行 + 审计到人」这三件事。
- GB/T 39786 密码应用要求:应用层的身份鉴别、访问控制与数据完整性都有对应条款——登录鉴别用密码技术,指令报文做完整性保护(签名),密钥全生命周期受控。本文链路里操作员签名、网关验签、台账整体签名分别对应这三处。
- 过程安全管理的变更与授权要求:化工行业对工艺变更历来有审批与授权要求,组态下装是典型的工艺变更动作。双人授权通道把行业里既有的变更管理流程落到技术上,检查时这两套话术可以互相印证:变更票对应双签名,下装记录对应变更台账。
- 矿山侧的行业要求:矿山安全生产的相关规程对提升机、主通风机、爆破器材库等点位的操作与监控有专门要求,身份与授权方案在这些点位要从严配置,并保留与行业检查口径一致的记录。
审查实操上,控制指令链路优先被问的是三件事:操作站是不是一人一账号、下装组态需要几个人、指令日志能不能追到人。这三问对应账号治理、职责分离、审计留痕三件事。把「登录日志 + 网关验签记录 + 下装双签名台账」一起调出来,举证会顺畅很多。证据留存统一四要素:时间、账号、指令摘要、结论——四要素齐了,内部复盘、上级检查、责任认定能引用同一份材料。
一句话边界:控制指令链路身份的合规清单是「等保三级 + GB/T 39786 + 过程安全变更管理」叠加,而技术落地的公约数只有一个——身份在人这一层做实,指令在网关这一层把关,控制器保持执行者角色。
07 | 落地答案:产品怎么承接
把三段授权链的合规要求翻译成落地组件,产品以「答案」身份出现,能力作主语:
- 统一身份与双因素:操作站、工程师站、上位机管理界面的账号收敛到统一身份源,一人一账号、离职即回收;操作站与工程师站的操作系统登录接入双因素认证代理,硬件因子由国密 UKEY 承担(插 Key 加口令,拔 Key 自动锁屏),登录动作在企业统一身份平台上留痕;离岗锁屏与会话超时按岗位配置。这一层解决的是「人是谁」——底账、双因子、离岗三件事一次收口。
- 签名验签与信任根:指令报文验签、组态下装双签名、台账整体签名都需要一套受保护的签名密钥。签名验签服务器承担批量验签,根密钥由硬件密码机保护、只在密码机内生成与使用、永不导出;密钥轮换支持重叠窗口,轮换前后的审计记录都能验。
- 指令与组态记录落盘加密:工程师站的组态版本库、上位机的指令台账与下装留痕启用驱动层透明加密,落盘即密文,应用无需改造;运维账号只见密文、审计账号按授权读取,防止「改记录的人同时能读记录」。这一层和装置内的数据保护是同一套底座,只是保护对象换成了变更与审计记录。
- 审计到人:登录、签发、验签、下装、轮换五类事件统一进审计台账,四要素齐全;审计账号独立于系统管理员,只看日志不改日志——职责分离在审计这一环同样成立。
这些组件的关系:统一身份平台管「人」,签名验签与密码机管「指令可信」,透明加密管「记录不可改」,审计台账管「说得清楚」。化工与矿山企业按三条线(账号与角色治理 / 指令链路加固 / 留痕与追溯)逐项对上,即为从操作员到 PLC 控制器的身份落地全路径。
08 | 验收清单与下一步
控制指令链路身份 验收清单
| 检查项 | 对应要求 | 验证点 | 是否落地 |
|---|---|---|---|
| 一人一账号一密钥 | 身份鉴别 | 登录日志的账号与人员一一对应 | □ |
| 双因子登录 | 等保三级 | 仅口令登录被拒 | □ |
| 离岗锁屏与会话超时 | 身份鉴别 | 离岗后旧会话失效 | □ |
| 指令报文签名 | 数据完整性 | 开度被改后验签失败 | □ |
| 冒名签发拦截 | 身份鉴别 | 换私钥签发被拒 | □ |
| 角色校验 | 访问控制 | 操作员签发下装被拒 | □ |
| 组态下装双人授权 | 变更管理 | 缺一把签名的下装不生效 | □ |
| 来源白名单 | 访问控制 | 未登记主机写请求被拦并告警 | □ |
| 台账整体签名加密 | 安全审计 | 改台账留原签名验签失败 | □ |
| 密钥轮换重叠窗口 | 密钥管理 | 轮换前后台账均可验 | □ |
趋势上,控制指令链路的身份要求会从「操作站有密码」走向「指令自证来源」:一方面监管检查对「追到人」的问法越来越具体,共用账号的举证成本会越来越高;另一方面集控化、少人化把远程操作的比例推高,远程会话的身份证明与指令签名绑定会成为常态要求。两个方向都指向同一件事——身份要跟着指令走,而不是跟着终端走。矿山侧的分点位定级授权也会逐步细化,安全关键点位的授权链会更严,一般点位追求轻量,方案设计时把分级做成可配置项,比事后重构省力得多。
下一篇预告:矿业化工线从「装置内」「数据出装置」「监测数据」「控制指令」走到法律层——《危险化学品安全法数据合规解读:从法律要求到备案数据安全》,从法律条文的义务主体与数据对象出发,讲备案数据的范围界定、存储与共享、变更留痕,把这条线从「标准驱动」接到「法律驱动」。
文章作者:安当加密-焱垚,安全工程师,专注身份认证、数据加密领域。

465

被折叠的 条评论
为什么被折叠?



