자정에 인증서가 만료됐는데 시계가 안 맞았다
Broker 인증서가 만료된 걸 알게 되는 방식은 대개 이렇다. 모든 gateway가 같은 순간에 끊기고, historian은 평평해지고, alarm summary가 connection failed로 가득 찬다. Client마다 한 줄씩, 어느 계층이 깨졌는지는 안 알려준다. Switch가 죽은 것처럼 보인다. 아니다. X.509 notAfter 시각이 지났을 뿐이고, RFC 5280은 이 시각을 UTC(Z time)로 정의한다. UTC+9 현장이라면 달력에 적힌 만료 시각보다 9시간 먼저 터진다.
MQTT-over-TLS 교체를 IT 티켓으로 다루면 안 되는 이유가 이거다. 웹 서버 인증서 갱신은 새로고침 한 번이다. Gateway 여러 대, HMI, historian, MES connector, cloud bridge 몇 개 앞을 막는 인증서 갱신은 공정 영향이 있는 commissioning 작업이다. Gateway가 publish를 멈추고, subscriber는 초록 아이콘 뒤에서 stale 값을 붙잡고, store-and-forward queue가 차오르고, Sparkplug node는 birth 순서를 통째로 다시 뿌린다. certbot renew가 아니라 cutover처럼 계획해야 한다.
손대기 전에 TLS handshake가 일어나는 지점을 전부 안다
Broker 인증서가 유일한 endpoint인 적은 없고, 물리는 건 늘 아무도 목록에 안 적은 그 하나다. 먼저 경로 전체를 훑고, 각 service가 인증서를 어디서 읽는지를 적는다 — file path인지, OS trust store인지, container secret인지, Kubernetes secret인지, 아니면 벤더만 아는 runtime folder인지.
| 항목 | 기록할 내용 | 무는 지점 |
|---|---|---|
| Broker server cert | CN/SAN, issuer, notAfter, key 위치 | Client가 접속하는 이름으로 broker identity를 확인함 |
| Broker CA chain | Root + 모든 intermediate | Intermediate 누락이 엄격한 client에서만 실패 — 설계상 들쭉날쭉 |
| Client cert (mTLS) | Gateway/app 이름, issuer, expiry | 각각이 이제 관리해야 할 별도 만료 시계 |
| Client trust store | Broker를 신뢰하는 CA | 신규 CA는 broker 교체 전에 들어가야 함 |
| Bridge credential | 상하위 파일 | Broker-to-broker link는 보통 자기 cert 쌍을 가짐 |
| Test tool | 노트북 cert와 trust store | 노트북은 되고 service는 안 되면 그냥 trust store 불일치 |
"어디서 읽는가" 칸을 빼면, 파일을 바꾸고 service를 restart했는데도 옛 cert를 계속 내미는 걸 보게 된다. 진짜 인증서가 당신이 모르는 store에 있었기 때문이다.
거의 모든 장애는 이름·시간·chain이지 암호가 아니다
Cipher가 틀려서 교체가 실패한 걸 한 번도 못 봤다. Identity와 validity에서 실패하니, 사전 점검은 거기에 둔다.
작업 창 전에 확인한다.
- Broker cert의 SAN에 client가 실제로 접속하는 이름이 전부 들어 있는지. 구역이 나뉜 plant/DMZ 망에서는 한 gateway는
mqtt-broker.local로, 다른 gateway는 raw IP로 붙는다. 둘 다 SAN에 있거나, 아니면 그 client를 재설정해야 한다. CN만 있는 cert는 죽었다. 요즘 client는 CN을 무시하고 SAN만 본다. - Client와 broker 시계가 맞는지. 20분 빠른 gateway는 새 cert를
not yet valid로 거부한다. - Intermediate 포함 full chain이 runtime이 원하는 순서로 조립됐는지.
keyUsage/extendedKeyUsage가 serverAuth(또는 client cert면 clientAuth)로 되어 있는지. 용도가 틀린 cert는 브라우저에선 통과하고 엄격한 MQTT 스택에선 거부된다.- Service account가 private key를 읽을 수 있고, 그 이상은 아무도 못 읽는지.
운영에 손대기 전에 대부분을 잡아 주는 명령 하나:
openssl s_client -connect broker:8883 -servername broker.host -showcerts </dev/null
Broker가 실제로 내미는 것 — 보내는 chain, 날짜, SAN — 을 배포했다고 믿는 파일이 아니라 네트워크에서 읽는다. 나머지는 파일에 대고 openssl x509 -noout -dates -ext subjectAltName으로 확인한다.
Trust를 먼저 추가하고 identity는 나중에 — 절대 동시에 말고
이 작업을 안전하게 만드는 규칙 하나: client는 CA 두 개를 동시에 신뢰할 수 있으니, identity를 좁히기 전에 trust를 넓힌다.
- 옛 broker cert가 아직 살아 있는 동안 신규 CA(자체 서명이면 신규 broker cert)를 client trust store에 넣는다. Client는 아직 안 쓰는 CA를 신뢰하게 됐다고 신경 쓰지 않는다.
- Pilot client 하나를 reload하고 옛 cert로 여전히 붙는지 확인한다.
- Broker cert를 교체한다.
- Pilot이 reconnect하고 진짜 payload를 publish하는지 본다. Handshake만이 아니라.
- 나머지를 순서대로 진행한다.
- 옛 CA는 정한 overlap 기간이 지난 뒤에만 제거한다 — 나는 1주를 준다. 작업 창 동안 꺼져 있던 gateway가 깨어났을 때도 broker를 신뢰하도록.
Mutual TLS면 client cert도 같은 방식으로 나눈다. 먼저 broker가 신규 client CA를 신뢰하게 하고, 그 다음 client 하나의 cert를 바꿔 authentication, topic authorization, retained/birth 동작을 그 하나에서 다시 증명한 뒤 나머지에 손댄다.
피할 방식은 big-bang이다 — broker identity와 모든 trust store를 한 방에. 그게 실패하면, 그리고 실패한다, 어느 쪽이 handshake를 거부했는지 아무것도 안 알려주고, 플랜트가 멈춘 채로 눈감고 롤백하게 된다.
시계를 돌리기 전에 buffer 계산을 한다
교체가 공정 데이터를 조용히 흘리면 안 된다. 작업 창 전에 edge buffer에서 네 숫자를 뽑는다. 현재 queue depth, 최대 queue size, publish rate, free disk. 그리고 나눈다.
초당 500개를 20분치 buffer에 밀어 넣는 gateway는 하드 한계를 준다 — cutover가 아무리 깨끗해도 1시간 창은 데이터를 잃는다. 계산이 안 맞으면 레버가 넷이다. 창 범위를 줄이거나, buffer를 키우거나, 그동안 중요치 않은 publish를 조이거나, 더 짧은 cutover로 쪼갠다. Queue alarm이 뜨는 22분째가 아니라 시작 전에 하나 고른다.
Replay 쪽에서 잊는 게 둘 있다. Reconnect를 건너 QoS와 session 설정이 실제로 message를 보존하는지 확인한다(clean-session에 QoS 0 client는 아무것도 안 남긴다). 그리고 buffer가 reconnect 시간이 아니라 원래 source timestamp로 replay하는지 확인한다 — 아니면 한 시간치 데이터가 cutover 시점 하나의 spike로 historian에 찍히고, 트렌드와 생산 기록이 거짓말을 한다.
초록 handshake가 아니라 MQTT 동작을 확인한다
TLS handshake 성공은 transport가 붙었다는 뜻이다. Client가 publish할 수 있는지, authorization이 살아남았는지, identity가 온전한지는 아무것도 안 말한다. 대표 client type마다 프로토콜 레벨에서 시험한다.
mosquitto_pub -h broker -p 8883 --cafile new-ca.pem \
--cert client.pem --key client.key \
-i real-client-id -t plant/line1/test -m rotation-check
그 다음 subscribe해서 payload, timestamp, quality field가 깨끗이 돌아오는지 확인하고, 허용 안 된 topic이 여전히 거부되는지 확인하고, client를 restart해 birth message를 보고, 강제로 disconnect해 last-will이 뜨는지 본다. 각각에 대해 broker log에서 cert subject, client ID, authorization 결과를 읽는다 — "인증은 됐는데 authorization은 실패" 장애가 숨는 곳이 그 로그 한 줄이고, client 쪽에서 보면 cert 문제처럼 보이지 않는다.
Sparkplug B를 쓰면 cert 변경이 새 논리 device를 만들면 안 된다. NBIRTH 순서와 metric alias가 교체 전에 이 node가 publish하던 것과 맞는지 확인한다. 새 bdSeq와 재등록된 alias로 돌아온 gateway는 MES 입장에서 사실상 새 device가 된 것이다 — 같은 선, 새 identity, 깨진 mapping.
운전원에게 계층을 가리키는 진단을 준다
빨간 통신 아이콘 하나는 보전팀에 아무것도 안 알려주고, 그래서 5분짜리 cert 거부와 끊긴 광케이블을 똑같이 — 느리게 — 다룬다. Secret은 안 흘리면서 계층은 구분되게 보여준다.
- Broker reachable인데 TLS 거부 → 네트워크 아니라 cert 문제.
- Cert expired / not yet valid → 시계와 날짜 확인.
- Name mismatch → 이 client가 접속하는 이름이 SAN에 없음.
- 인증은 됐는데 topic authorization 실패 → TLS 아니라 ACL.
- Queue depth + 가장 오래된 queued timestamp + 마지막 successful publish time → 얼마나 buffer됐고 얼마나 밀렸는지.
이 중 어느 것도 key를 노출하지 않는다. 무전기 든 사람을 switch가 아니라 맞는 계층으로 향하게 할 뿐이다.
교체마다 반복되는 고장 형태
Broker 교체 뒤 일부 gateway만 실패한다. 살아남은 쪽은 SAN에 있는 이름으로 붙고, 죽은 쪽은 없는 이름 — 대개 raw IP나 다른 DNS 구역 — 으로 붙는다.
노트북에선 되고 service에선 실패한다. Trust store가 다르다. 항상 runtime host의 runtime 계정에서 시험한다. 노트북은 당신을 속이고 있다.
Intermediate를 넣자마자 실패한다. 그 runtime에 대해 chain 순서나 file format이 틀렸다. Leaf → intermediate → root 연결을 원하는 것도, trust store에 CA cert만 원하고 leaf가 섞인 bundle을 거부하는 것도 있다.
Reconnect 후 데이터가 두 배로 들어온다. Store-and-forward replay와 QoS retry가 둘 다 뛰었다. Message ID, source timestamp, subscriber의 idempotent 처리를 확인한다.
달력에 적은 만료가 아니라 딱 떨어지는 시각에 전부 끊긴다. notBefore/notAfter는 UTC다. 당신 달력은 아니었다. 첫 문단 참고.
cert renewed 말고 증거를 남긴다
이번 기록이 있으면 다음 교체는 절반이 끝난 셈이다. 어떤 cert와 serial number, 이전·신규 issuer, 만료일, 시험한 client 목록, broker listener, 작업 전후 queue depth, 수동 restart가 필요했던 client, 옛 trust material 제거 예정일. 티켓에 cert renewed 한 줄은 다음 사람이 당신이 방금 저지른 실수를 전부 반복하도록 보장한다 — UTC 그것까지 포함해서.