OpenVPN 원격 접속 VPN — 세우고, 길을 놓고, 잠그고, 고친다
문 앞에서 버리고, 떠난 사람은 끊고, 붙은 사람도 좁게
한 줄 요약
인증서는 「누구인가」 를 확인할 뿐, 확인하기까지의 TLS 는 누구에게나 해 줍니다. tls-crypt 로 키 없는 패킷은 문 앞에서 버리고, 떠난 사람은 CRL 로 끊되 CRL 자체의 유통기한을 챙기고, 서버는 권한을 내려놓고, 붙은 사람도 필요한 곳만 가게 합니다.
왜 이게 필요했나
1모듈의 서버는 UDP 1194 에 닿는 누구와도 TLS 핸드셰이크를 시작합니다. 이 실습 환경에서 우리 CA 가 아닌 스스로 서명한 인증서(mallory)로 붙어 보면 서버는 결국 VERIFY ERROR: … self-signed certificate … CN=mallory 로 거절하지만, 그 전에 mallory 쪽에는 VERIFY OK: depth=0, CN=vpn.corp.example 이 찍힙니다 — 낯선 사람이 서버 인증서와 우리 CA 의 이름을 받아 간 것입니다. 그리고 TLS 라이브러리의 취약점은 바로 이 「인증 전」 구간에서 터집니다. 인터넷에 열린 VPN 은 하루에도 수없이 두드려지므로, 문 앞에서 버리는 장치가 하나 더 필요합니다.
어떻게 동작하나
tls-crypt. 서버와 모든 직원이 나눠 갖는 대칭 키 하나로 제어 채널 패킷 전체를 암호화하고 인증합니다. 키가 없는 패킷은 풀리지 않으므로 TLS 엔진에 닿기 전에 버려지고, 서버는 답조차 하지 않습니다. 이 환경의 서버 기록에는 TLS Error: tls-crypt unwrapping failed from [AF_INET]172.30.1.4:… 한 줄만 남고 mallory 에 대한 VERIFY 줄은 사라집니다. 같은 목적의 tls-auth 는 서명만 해서 제어 채널의 내용(직원 인증서의 이름)이 밖에서 보이지만, tls-crypt 는 감싸서 숨깁니다. 이 키는 신원이 아니라 문 앞의 암호라, 한 사람이 새 나가면 모두의 키를 바꿔야 합니다(그래서 OpenVPN 2.6 에는 사람마다 다른 키를 주는 tls-crypt-v2 가 있습니다).
폐기와 CRL. 퇴사자의 인증서는 유효 기간이 남아 있어도 끊어야 합니다. easyrsa revoke 는 CA 장부(index.txt)에 R 을 적는 일이고, 서버가 그것을 알게 하는 것은 CA 가 서명한 폐기 목록 파일(CRL)입니다. revoke → gen-crl → 서버 자리에 배포 → crl-verify 가 한 묶음입니다. 이 환경에서 폐기한 bob 은 VERIFY ERROR: depth=0, error=certificate revoked: CN=bob 으로 거절되고 alice 는 그대로 붙어 있었습니다. OpenVPN 은 CRL 파일이 바뀌면 다음 접속부터 다시 읽지만, 이미 붙은 세션은 다음 재협상(기본 1시간)까지 남으므로 급하면 서버를 다시 켜거나 관리 인터페이스로 그 세션을 끊습니다.
CRL 의 유통기한. CRL 에는 다음 갱신 시각(nextUpdate)이 있습니다. easy-rsa 3.1.7 의 기본은 180일이고, 이 환경에서 2026-10-31 에 찍은 CRL 의 nextUpdate 는 2027-04-29 였습니다. 그 시각이 지나면 OpenVPN 은 만료된 CRL 로는 아무도 확인할 수 없다며 모든 클라이언트를 거절합니다. 이 환경에서 nextUpdate 를 3초 뒤로 잡은 CRL 을 놓아 보자, 폐기하지 않은 alice 가 VERIFY ERROR: depth=0, error=CRL has expired: CN=alice 로 거절됐습니다. 폐기한 사람이 한 명뿐이어도 반년 뒤 어느 아침 전 직원이 못 들어옵니다. 그래서 CRL 은 타이머로 주기적으로 다시 찍어 배포합니다.
권한 내려놓기. OpenVPN 은 root 로 시작해 tun 을 만들고 키를 읽은 뒤 user nobody 로 내려갑니다. 그 뒤로 접속마다 다시 읽는 것(crl.pem, ccd 파일)은 nobody 가 읽을 수 있어야 하고, persist-key·persist-tun 이 있어야 재시작 때 키를 다시 읽거나 tun 을 다시 만들 필요가 없습니다. 서버 프로세스가 뚫려도 공격자가 얻는 것은 nobody 입니다.
붙은 사람도 좁게. VPN 에 붙었다는 것이 사내망 어디든 가도 된다는 뜻은 아닙니다. ccd 로 사람에게 고정 VPN 주소(ifconfig-push)를 주면 게이트웨이의 포워딩 규칙을 사람 단위로 쓸 수 있습니다. 이 환경에서 alice 를 10.8.0.10 으로 고정하고 「10.8.0.10 → 사내 서버 80/tcp」 만 넘기자 위키는 열리고 같은 서버의 22번은 닫혔습니다. AWS Client VPN 의 인가 규칙과 보안 그룹이 같은 일을 그룹 단위로 합니다.
현장에서 만나는 모습
「CRL 만료로 전사 VPN 장애」 는 OpenVPN 운영에서 가장 흔한 대형 사고 가운데 하나입니다. 처음 세울 때는 폐기할 사람이 없어 CRL 을 안 쓰다가, 첫 퇴사자가 생겼을 때 급히 gen-crl 을 한 번 하고 잊어버리는 것이 전형적인 순서입니다. 반대로 tls-crypt 키를 직원 설정 파일에 통째로 넣어 배포하면서 그 파일을 사내 위키에 올려 두는 경우도 흔합니다 — 그 키는 비밀입니다.
다음 실습에서 할 것
mallory 로 서버가 낯선 인증서에도 핸드셰이크하는 것을 보고 tls-crypt 로 닫습니다. bob 을 만들었다가 폐기하고, CRL 을 다시 찍는 타이머를 걸고, 서버를 nobody 로 돌리고, alice 를 사내 위키에만 들여보냅니다.