VPN 루트 CA를 오프라인으로 유지하면서도 인증서 발급하기
루트 키를 네트워크에서 분리하고, 중간 CA로 리프 인증서에 서명하고, CRL을 제때 게시하고, 첫 클라이언트가 연결되기 전에 체인을 테스트하세요.
VPN은 새벽 3시에도 연결에 응답하지만, 인증서에 서명하는 기계는 서랍 속에서 꺼진 채로 있습니다. 서버 인증서를 검증하는 클라이언트는 절대 CA에 접속하지 않으므로, CA는 어떤 연결에도 도달 가능할 필요가 없습니다. 계획이 필요한 부분은 인증서를 발급하거나 갱신하거나 폐기해야 하는 날 루트 키를 잠근 채로 두는 일입니다.
연결이 실제로 검증하는 것
OpenVPN 클라이언트가 연결하면 서버의 리프 인증서에서 이미 신뢰하는 CA 인증서까지 서명 체인을 따라 올라가고, 유효 기간을 확인하며, 프로필이 그렇게 지정하면 폐기 목록을 조회합니다. 루트 CA를 찾지는 않습니다. 서버는 클라이언트 인증서에 대해 같은 일을 거울처럼 수행합니다. 연결 시점에는 리프, 중간, 이전에 배포한 신뢰 번들만 존재합니다.
실제로 실패하는 것은 세 가지 중 하나입니다. 발급 시점에 서명 키를 사용할 수 없거나, 폐기 목록이 오래되었거나, 클라이언트의 신뢰 번들이 서버가 제시하는 체인보다 오래된 경우입니다. 첫 번째는 프로세스 문제이고 나머지 둘은 배포 문제입니다. 어느 것도 루트 키가 온라인일 것을 요구하지 않으며, 터널 인증에 개인 인증서를 요구하는 관리형 VPN 서비스는 서명 권한을 공급자에게 옮길 뿐입니다. Azure 지점 간 연결은 반대로 작동합니다. 루트 인증서를 업로드하면 그 아래에서 서명된 인증서가 신뢰되므로, 누가 서명하고 누가 검증하며 인증서를 어떻게 조기에 철회하는지 물어보세요.
주소를 절대 받지 않는 호스트에서 루트를 구축하세요
오프라인 루트는 거의 사용되지 않습니다. 중간 CA 인증서에 서명할 때와 나중에 그것을 갱신하거나 폐기할 때뿐입니다. 나머지는 모두 한 단계 아래에서 일어납니다. 루트 작업은 네트워크에서 분리된 기계에서, 가능하면 라이브 시스템에서 수행하고 누가 방에 있었는지 기록하세요. 명령은 OpenSSL 1.1.1 이상을 가정합니다. -addext은 이전 빌드에서 사용할 수 없기 때문입니다.
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out root.key
openssl req -x509 -new -key root.key -sha256 -days 7300 \
-subj "/CN=Example VPN Root CA" -out root.crt \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign"
서명 호스트에서 중간 인증서를 만들고 그 요청만 오프라인 기계로 가져갑니다. 루트는 그 요청을 경로 길이 영으로 서명하므로, 중간 CA는 끝점 인증서에 서명할 수 있지만 그 아래에 또 다른 CA를 만들 수는 없습니다. 그 하나의 확장이 침해된 서명 호스트가 루트와 동등한 권한을 만드는 것을 막습니다. 모든 클라이언트가 OpenVPN 2.4 이상을 실행할 때만 ECDSA를 사용하고, 그렇지 않으면 RSA 키를 생성하고 흐름을 동일하게 유지하세요.
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out intermediate.key
openssl req -new -key intermediate.key -subj "/CN=Example VPN Intermediate CA" -out intermediate.csr
openssl x509 -req -in intermediate.csr -CA root.crt -CAkey root.key \
-CAcreateserial -days 3650 -sha256 -extfile intermediate.ext -out intermediate.crt
intermediate.ext에는 basicConstraints=critical,CA:TRUE,pathlen:0, keyUsage=critical,keyCertSign,cRLSign, authorityKeyIdentifier=keyid:always이 포함됩니다. 서명 호스트를 다시 연결하기 전에 루트 키를 금고에 돌려놓으세요. 루트가 중간 인증서를 폐기할 수 있어야 한다면 openssl x509 -req 대신 루트 수준의 index.txt에 대해 openssl ca -config root.cnf -extensions intermediate_ca로 서명하여 일련번호가 기록되게 하세요. 그런 다음 결과 루트 목록을 클라이언트 프로필 안에 배포하고, 파일 기반 crl-verify이 시작할 때 그것을 읽습니다. 클라이언트에는 두 CA 인증서가 모두 담긴 번들을 보내세요. 리프만으로는 루트에 대해 검증되지 않습니다.
중간 CA에서 리프를 발급하세요, 데이터베이스와 함께
openssl x509 -req로 서명하는 것은 일련번호를 기록하는 것이 없기 때문에 폐기가 필요할 때까지 작동합니다. 첫 인증서부터 openssl ca를 사용하여 일련번호, 만료, 폐기 상태가 하나의 데이터베이스에 있게 하세요. 데이터베이스 파일을 한 번 만들고 나서 구성을 그것들로 향하게 하세요:
mkdir -p /etc/vpn-ca/intermediate/newcerts
touch /etc/vpn-ca/intermediate/index.txt
echo 1000 > /etc/vpn-ca/intermediate/serial
echo 1000 > /etc/vpn-ca/intermediate/crlnumber
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = /etc/vpn-ca/intermediate
database = $dir/index.txt
new_certs_dir = $dir/newcerts
serial = $dir/serial
crlnumber = $dir/crlnumber
certificate = $dir/intermediate.crt
private_key = $dir/intermediate.key
default_md = sha256
default_days = 365
default_crl_days = 30
unique_subject = no
policy = policy_any
[ policy_any ]
commonName = supplied
[ server_cert ]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature
extendedKeyUsage = serverAuth
subjectAltName = @server_names
[ client_cert ]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature
extendedKeyUsage = clientAuth
[ server_names ]
DNS.1 = vpn.example.com
키 사용 줄은 ECDSA 키를 가정하므로 RSA의 경우 keyEncipherment를 추가하고, 각 클라이언트에 고유한 일반 이름을 부여하세요. 왜냐하면 OpenVPN 그 필드로 연결을 식별하고, CN=vpn-client을 공유하는 두 노트북은 로그에서도, client-config-dir 규칙에서도, 사용자별 푸시에서도 구분할 수 없으며, 폐기 결정을 방어 가능하게 만드는 귀속 정보를 잃게 됩니다. 리프를 다음과 같이 발급하고 검증하세요:
openssl req -new -key alice.key -subj "/CN=alice-laptop" -out alice.csr
openssl ca -config intermediate.cnf -extensions client_cert -in alice.csr -out alice.crt -batch
openssl verify -CAfile ca-bundle.crt -untrusted intermediate.crt alice.crt
서명 키를 VPN 호스트에 복사하지 말고 정책 경계 뒤에 두어, 요청을 인증하고 일반 이름을 허용 목록과 대조하고 인증서만 반환하는 작은 서비스로 만드세요. 모든 일련번호를 기록하고, 로그는 인증서와 그것을 요청한 사람을 잇는 유일한 연결고리입니다.
루트를 깨우지 않고 폐기를 게시하세요
클라이언트 인증서는 중간 CA가 폐기하며 루트가 폐기하는 일은 없습니다. 루트 목록은 중간 CA가 소각되었을 때만 중요합니다. crl-verify로 서버를 중간 목록으로 향하게 하세요. 파일 기반 CRL은 서비스 시작 시 로드되므로 재생성할 때마다 다시 로드하세요. 디렉터리 형태는 연결 시점에 상태를 읽고 일련번호마다 파일 하나를 소비합니다.
같은 데이터베이스에서 두 명령으로 폐기하고 재생성하세요:
openssl ca -config intermediate.cnf -revoke /etc/vpn-ca/intermediate/newcerts/1001.pem
openssl ca -config intermediate.cnf -gencrl -out /etc/openvpn/crl.pem
그다음 시계를 지켜보세요. CRL에는 다음 업데이트 필드가 있고, 그 날짜가 지나면 검증하는 피어는 목록을 무시하지 않고 거부합니다. default_crl_days = 30로 매달 재생성하면 여유가 생기고, 매년 하면 그렇지 않습니다. 리프 수명을 짧게 하는 것이 더 신뢰할 수 있는 통제입니다. 폐기는 목록을 조회하는 곳에서만 적용되기 때문입니다.
체인을 테스트한 다음 의식을 문서로 남기세요
설계를 신뢰하기 전에 네 가지 실패를 재현하세요. openssl verify -CAfile ca-bundle.crt -untrusted intermediate.crt server.crt로 체인을 검증하세요. 만료된 클라이언트 인증서로 연결하고 서버가 거부하는지 확인하세요. 테스트 클라이언트를 폐기하고 다시 로드한 뒤 목록이 효력을 발휘하는지 확인하세요. 호스트 시계를 하루 앞으로 돌리세요. 잘못된 시계는 다른 곳에서는 유효해 보이는 인증서를 발급하고 거부할 수 있기 때문입니다.
오프라인 루트는 서류가 있을 때만 통제입니다. 의식을 위해 이름이 지정된 두 사람, 서명된 모든 일련번호의 기록, 기계와 별도로 보관하는 암호화된 백업, 격리된 호스트에서의 복원 테스트가 필요합니다. 같은 의식에서 예비 중간 CA에 서명해 두면 서명 호스트가 죽어도 자정에 루트를 열 필요가 없습니다.
완료라고 부르기 전에 다섯 가지를 확인하세요. 루트 키가 네트워크에 연결된 호스트에 없어야 하고, 중간 키는 서명 호스트에만 있어야 하며, 클라이언트 번들에 두 CA 인증서가 모두 있어야 하고, CRL에 미래의 다음 업데이트가 있어야 하며, 체인의 모든 만료에 대해 누군가가 캘린더 항목을 가지고 있어야 합니다. 하나라도 빠지면 오프라인처럼 보이는 구성은 그저 문서화되지 않은 것입니다.