인증서 만료일이 10월 14일이라고 적혀 있었다. 그래서 작업을 14일 밤 10시로 잡았다. 통신은 그날 오후 3시에 끊겼다.
X.509 인증서의 notAfter는 지역 시간이 아니다. RFC 5280 §4.1.2.5.1은 UTCTime 값을 GMT(Zulu)로, 초 단위까지 넣어 표기하도록 요구한다. KST는 UTC+9다. notAfter=Oct 14 06:02:48 2031 GMT인 인증서는 한국에서 10월 14일 15시 02분 48초에 죽는다. 아홉 시간은 야간 작업 계획 하나를 통째로 날리기에 충분하다.
저장소가 두 개라는 것부터
갱신을 한 단계로 생각하면 반만 하고 끝난다. OPC UA 애플리케이션은 인증서를 두 종류로 들고 있다.
- 자기 인증서(application instance certificate, 보통
own/certs+own/private). 자기가 누구인지 증명하는 것. 개인 키가 붙어 있다. - 신뢰 목록(
trusted/certs,trusted/crl,issuers/certs). 남을 믿어 주는 근거. 개인 키가 없다.
서버의 자기 인증서를 바꾸는 작업은 서버 한 대에서 끝난다. 그런데 그 인증서를 믿어야 하는 쪽은 클라이언트 전부다. HMI, 히스토리언 수집기, 엣지 게이트웨이, 리포트 서버, 엔지니어링 노트북. 신뢰 목록은 대수만큼 따로 있다. 갱신 당일 세 대가 안 붙는 이유는 거의 항상 이것이고, 서버 로그는 깨끗하다.
자기 서명(self-signed) 인증서를 쓰면 이 작업이 N번이다. CA가 발급한 인증서를 쓰고 클라이언트가 CA 인증서를 issuers/certs에 들고 있으면, 서버 인증서를 갱신해도 클라이언트는 손댈 필요가 없다. 설비가 다섯 대를 넘어가면 CA를 세우는 쪽이 싸다. 현장 사설 CA 하나로 충분하다.
넣기 전에 인증서를 읽어 본다
벤더 UI가 "인증서 생성" 버튼 하나로 끝내 주는 경우가 많고, 그 버튼이 무엇을 만들었는지는 아무도 보지 않는다. der 파일을 그냥 읽으면 된다.
openssl x509 -in own/certs/MyServer.der -inform der -noout -dates -ext subjectAltName
openssl x509 -in own/certs/MyServer.der -inform der -noout -text \
| grep -E "Public-Key|Signature Algorithm"
실제 출력은 이렇게 나온다.
notBefore=Oct 15 06:02:48 2026 GMT
notAfter=Oct 14 06:02:48 2031 GMT
X509v3 Subject Alternative Name:
URI:urn:plc-gateway-01:Vendor:UaServer, DNS:plc-gateway-01, IP Address:10.20.30.40
Signature Algorithm: sha256WithRSAEncryption
Public-Key: (2048 bit)
이 네 줄에서 확인할 것.
notAfter를 KST로 환산한다. 위 인증서는 2031-10-14 15:02:48 KST에 끝난다. 작업 창을 그 앞에 잡는다.URI:항목이 서버의applicationUri와 글자 단위로 같은지. IEC 62541-6은 ApplicationUri가 SubjectAltName의uniformResourceIdentifier항목으로 들어가도록 요구한다. 한 글자라도 다르면Bad_CertificateUriInvalid다. VM을 복제하고 호스트명만 바꿨을 때 여기서 걸린다. 증상 구분은 ping은 되는데 접속을 거부하는 경우에 정리해 뒀다.- 클라이언트가 실제로 거는 이름이
DNS:또는IP Address:로 다 들어 있는지. DNS로 거는 클라이언트와 IP로 거는 클라이언트가 섞여 있으면 둘 다 넣어야 한다. 빠진 쪽은Bad_CertificateHostNameInvalid를 받는다. NAT 뒤라면 외부 이름도 포함한다. - 키 길이와 서명 알고리즘. SHA-256 계열 SecurityPolicy는 RSA 2048~4096 비트를 쓴다.
sha1WithRSAEncryption으로 서명된 인증서가 보이면 그건 갱신 대상이 아니라 폐기 대상이다.
갱신은 정책을 올릴 기회다
Basic128Rsa15와 Basic256은 SHA-1에 기대고 있어서 OPC UA 1.04에서 폐기(deprecated)됐다. 현재 쓸 것은 Basic256Sha256, Aes128_Sha256_RsaOaep, Aes256_Sha256_RsaPss다(IEC 62541-7 Profiles). 인증서를 어차피 새로 만드는 날이 정책을 올리기 가장 싼 날이다. 나중에 따로 하려면 같은 중단을 두 번 낸다.
MessageSecurityMode도 같이 본다. 문제 해결하다가 None으로 내려놓고 올리는 걸 잊으면, 그 뒤로 인증서가 만료돼도 아무 일이 일어나지 않는다. 연결은 멀쩡하고 보안은 없다. 조용해서 더 나쁘다. 정책별 증상 차이는 None으로는 붙는데 Basic256Sha256으로는 끊기는 경우를 본다.
서버가 push 모델을 지원하면 파일을 만지지 않는다
IEC 62541-12는 인증서 관리를 OPC UA 서비스로 정의한다. 서버 주소 공간의 ServerConfiguration 객체 아래에 method가 있다.
| Method | 하는 일 |
|---|---|
CreateSigningRequest | 서버가 CSR을 만들어 준다. 개인 키가 장비 밖으로 나가지 않는다. |
UpdateCertificate | 서명된 인증서를 밀어 넣는다. applyChangesRequired를 돌려준다. |
ApplyChanges | 새 인증서를 실제로 적용한다. 보안 채널이 끊긴다. |
GetRejectedList | 서버가 거절한 인증서 목록. |
여기서 두 가지가 중요하다.
UpdateCertificate의 출력 applyChangesRequired가 true면 ApplyChanges를 불러야 적용된다. 그리고 ApplyChanges가 세션을 끊는다. 즉 중단 시점은 서비스 재시작이 아니라 method 호출 한 번이고, 그 시각을 네가 정한다. 히스토리언 트렌드에 생기는 구멍은 이 시각을 기준으로 설명된다. 작업 기록에 적을 시각도 이것이다.
GetRejectedList는 갱신 당일 가장 쓸모 있는 호출이다. 서버가 거절한 클라이언트 인증서를 돌려주므로, 어느 클라이언트가 실패했는지 알아내려고 장비 열 대의 로그를 뒤질 필요가 없다. 재연결이 끝난 뒤 이 목록이 비어 있어야 한다.
인증서 그룹이 여러 개인 것도 놓치기 쉽다. DefaultApplicationGroup, DefaultHttpsGroup, DefaultUserTokenGroup. 애플리케이션 그룹만 갱신하면 나머지는 그대로 만료일을 향해 간다.
폐기 목록은 아무도 안 닫는다
키가 유출돼서 갱신하는 경우라면, 새 인증서를 넣는 것으로 끝이 아니다. 헌 인증서를 못 쓰게 만들어야 한다. 그게 CRL이고, 신뢰 목록의 trusted/crl 자리가 그 용도다.
문제는 CRL이 없을 때의 동작이다. 발급자의 CRL을 못 구하면 검증 결과는 Bad_CertificateRevocationUnknown인데, 많은 스택이 이걸 무시하도록 기본 설정돼 있다. 그러면 폐기한 인증서로도 그냥 붙는다. 폐기했다고 믿고 있는 상태가 제일 위험하다.
자기 서명 인증서는 CRL 자체가 없다. 폐기 = 모든 상대 장비의 신뢰 목록에서 손으로 지우기, N번. 여기서도 CA가 답이다.
합격 기준 열 가지
초록색 아이콘은 증거가 아니다. 아래는 값을 강제로 틀려 보면서 확인하는 항목이다. 네 번째와 다섯 번째가 핵심이다 — 나머지는 "되는지" 보는 것이고, 이 둘은 "검증을 실제로 하고 있는지" 보는 것이다.
openssl x509 -noout -dates로notAfter를 읽고 KST로 환산한다. 남은 일수를 기록한다.- SAN의
URI:항목을 서버applicationUri와 글자 단위로 비교한다. 눈으로 비슷한 건 같은 게 아니다. - 클라이언트가 쓰는 모든 이름으로 접속해 본다. DNS, IP, NAT 외부 이름. 하나씩 전부.
- 벤치에서 클라이언트 시계를
notAfter이후로 돌려 본다.Bad_CertificateTimeInvalid가 나와야 한다. 그냥 붙으면 그 클라이언트는 유효기간을 보지 않는다. - 클라이언트 한 대의 신뢰 목록에서 새 서버 인증서를 지우고 접속한다.
Bad_CertificateUntrusted가 나와야 한다. 붙으면 그 클라이언트는 자동 신뢰 상태이고, 그 링크는 보안이 아니다. 엔지니어링 도구에서 특히 흔하다. 확인 뒤 다시 넣는다. GetRejectedList가 비어 있다. 운영 클라이언트 전부가 재연결을 마친 뒤에 본다.- 각 세션의 MessageSecurityMode가
SignAndEncrypt로 읽힌다.None으로 조용히 내려간 세션이 없다. ServerDiagnosticsSummary의securityRejectedSessionCount가 더 이상 오르지 않는다(IEC 62541-5). 올라가면 아직 못 붙은 클라이언트가 있다.- 히스토리언 트렌드의 빈 구간이
ApplyChanges시각으로 설명된다. 그보다 길면 재연결을 못 한 수집기가 있다. - 헌 인증서가 모든 상대가 실제로 가진 CRL에 올라가 있거나, 모든 신뢰 목록에서 지워져 있다. "폐기했다"는 말만 남아 있으면 안 된다.
이중화 서버는 대기 쪽을 먼저 갱신하고, 페일오버해서 1~9번을 다시 돌리고, 반대쪽을 한다. 두 노드의 applicationUri가 서로 다르다는 것도 이때 확인한다 — 이중화 failover 시험에 적어 둔 함정이다.
다음에 확인할 것
갱신이 끝났으면 프로젝트 폴더에 네 줄만 남긴다. subject와 thumbprint, applicationUri, notAfter를 UTC와 KST 둘 다, 그리고 ApplyChanges를 부른 시각.
그리고 한 가지가 남는다. 복원 리허설에서 헌 VM 스냅샷을 되살리면 만료된 인증서가 같이 돌아온다. 백업 세트에 인증서가 어떻게 들어 있는지는 깨끗한 장비 복원 리허설에서 확인하고, 인증서와 같이 묶여 있는 service account 쪽은 계정 비밀번호 교체를 본다. 시운전 때 서버가 고쳐 돌려준 값을 적어 두는 습관은 클라이언트 시운전 체크리스트에 있다.