TT Lab
블로그 시작하기
배우기 러닝패스 코스

OpenVPN 원격 접속 VPN — 세우고, 길을 놓고, 잠그고, 고친다

잠그기 — tls-crypt·CRL·nobody·직원별 접근

TT Lab 에서 이어서 보기

목표

붙기만 하는 VPN 을 운영할 수 있는 VPN 으로 바꿉니다. 낯선 사람에게는 답조차 하지 않게(tls-crypt), 퇴사자는 바로 끊기게(CRL), 그 CRL 이 만료돼 모두가 끊기는 사고를 막고(타이머), 서버가 뚫려도 root 가 아니게(nobody), 붙은 사람도 필요한 곳만 가게(고정 주소 + 포워딩 규칙) 만듭니다.

왜 중요한가

VPN 게이트웨이는 인터넷에 열린 몇 안 되는 문이라 늘 두드려집니다. 인증서만으로는 「누구인가」 를 확인할 뿐, 확인하기까지의 TLS 코드와 CPU 를 낯선 사람에게도 내줍니다. 그리고 VPN 사고의 상당수는 공격이 아니라 운영에서 납니다 — 퇴사자 인증서가 살아 있거나, CRL 이 조용히 만료돼 어느 아침 전 직원이 못 들어오거나.

단계

  1. laptop 에서 스스로 서명한 인증서(CN mallory, 우리 CA 가 아님)로 openvpn --config 를 12초쯤 돌려 보세요(/root/mallory/ 에서, --dev tun9). 그동안 서버 기록(journalctl -u openvpn-server@corp)에 남은 VERIFY 줄을 /root/vpn/sec/before-tc.txt 에 저장하세요. 서버가 mallory 를 거절한 VERIFY ERROR 줄이 있어야 합니다.
  2. openvpn --genkey tls-crypt /etc/openvpn/server/tc.key(권한 600)로 키를 만들고 서버 설정에 tls-crypt tc.key 를 더하세요. 같은 키를 laptop 의 /etc/openvpn/client/tc.key(600)로 옮기고 alice.conf 에도 tls-crypt tc.key 를 더한 뒤 서버와 클라이언트를 다시 켭니다. alice 는 다시 붙어야 합니다.
  3. 1단계의 mallory 를 그대로 다시 돌리고, 그동안의 서버 기록을 /root/vpn/sec/after-tc.txt 에 저장하세요. tls-crypt unwrapping failed 줄이 있고, mallory 에 대한 VERIFY 줄은 없어야 합니다.
  4. alice 와 같은 방법(키는 laptop 에서, gateway 는 서명만)으로 CN bob 의 클라이언트 인증서를 만들고, laptop 에 /etc/openvpn/client/bob.conf(tls-crypt 포함)를 써서 openvpn-client@bob 을 켜세요. laptop 에 tun 이 둘(alice·bob) 생기고 bob 의 tun 으로도 10.8.0.1 이 응답해야 합니다.
  5. ./easyrsa revoke bob 과 ./easyrsa gen-crl 로 CRL 을 만들고 /etc/openvpn/server/crl.pem(권한 644)으로 놓은 뒤 서버 설정에 crl-verify crl.pem 을 더해 서버를 다시 켜세요. bob 은 다시 붙지 못하고(서버 기록에 certificate revoked), alice 는 계속 붙어 있어야 합니다. 거절을 확인했으면 laptop 의 openvpn-client@bob 을 끄세요(disable --now) — 퇴사자의 노트북은 회수했다고 칩니다.
  6. CRL 을 다시 찍어 서버 자리에 놓는 crl-renew.service(oneshot: easyrsa gen-crl → install -m 644 … /etc/openvpn/server/crl.pem)와 매주 도는 crl-renew.timer 를 만들어 켜고(enable --now), 서비스를 한 번 돌리세요. 그다음 /etc/openvpn/server/crl.pem 의 다음 갱신 시각(nextUpdate)을 /root/vpn/sec/crl-next.txt 에 한 줄로 적습니다.
  7. 서버 설정에 user nobody, group nogroup 을 더하고 다시 켜세요. 서버의 openvpn 프로세스가 nobody 로 돌고, alice 는 다시 붙어야 합니다.
  8. client-config-dir ccd 를 켜고 /etc/openvpn/server/ccd/alice 에 ifconfig-push 10.8.0.10 255.255.255.0 을 적어 alice 에게 고정 주소를 주세요. gateway 에 nft 표 inet vpn_acl(원본 /etc/vpn-acl.nft, 재부팅에도 남게)을 걸어 tun0 에서 들어온 것 중 10.8.0.10 → 10.10.0.10:80/tcp 와 이미 맺은 연결의 답만 넘기고 나머지는 버리세요. laptop 에서 http://10.10.0.10/whoami 는 10.8.0.10, nc -z -w3 10.10.0.10 22 는 실패해야 합니다.

참고

낯선 인증서에도 서버는 핸드셰이크를 끝까지 한다

laptop 에서 스스로 서명한 인증서(CN mallory, 우리 CA 가 아님)로 openvpn --config 를 12초쯤 돌려 보세요(/root/mallory/ 에서, --dev tun9). 그동안 서버 기록(journalctl -u openvpn-server@corp)에 남은 VERIFY 줄을 /root/vpn/sec/before-tc.txt 에 저장하세요. 서버가 mallory 를 거절한 VERIFY ERROR 줄이 있어야 합니다.

거절은 했지만 거절하기까지 서버는 TLS 핸드셰이크를 진행했고, mallory 쪽 기록에는 서버 인증서(VERIFY OK: depth=0, CN=vpn.corp.example)까지 남습니다. UDP 1194 에 닿는 누구에게나 서버가 인증서를 보여 주고 TLS 코드를 돌려 주는 셈입니다 — 공격면이자 CPU 를 태우는 문입니다. self-signed 인증서는 openssl req -x509 -newkey rsa:2048 -nodes -subj /CN=mallory -keyout m.key -out m.crt -days 1.

tls-crypt — 키 없는 패킷은 TLS 전에 버린다

openvpn --genkey tls-crypt /etc/openvpn/server/tc.key(권한 600)로 키를 만들고 서버 설정에 tls-crypt tc.key 를 더하세요. 같은 키를 laptop 의 /etc/openvpn/client/tc.key(600)로 옮기고 alice.conf 에도 tls-crypt tc.key 를 더한 뒤 서버와 클라이언트를 다시 켭니다. alice 는 다시 붙어야 합니다.

tls-crypt 는 제어 채널 패킷 전체를 미리 나눈 대칭 키로 암호화·인증합니다. 키가 없는 쪽의 패킷은 TLS 엔진에 닿기 전에 버려지고, 서버는 답조차 하지 않습니다. 이 키는 모든 직원이 나눠 갖는 「문 앞의 암호」 라 신원 확인은 여전히 인증서가 합니다. 키를 옮길 때는 scp 로, 권한은 600.

같은 낯선 사람을 다시 — 이번에는 TLS 까지 가지도 못한다

1단계의 mallory 를 그대로 다시 돌리고, 그동안의 서버 기록을 /root/vpn/sec/after-tc.txt 에 저장하세요. tls-crypt unwrapping failed 줄이 있고, mallory 에 대한 VERIFY 줄은 없어야 합니다.

이제 서버는 mallory 의 첫 패킷을 풀지 못해 버립니다. 인증서를 보여 주지도, TLS 를 시작하지도 않습니다. tls-auth(서명만)와 달리 tls-crypt 는 제어 채널을 암호화까지 해서, 접속 중에 오가는 인증서(직원 이름)도 밖에서 보이지 않습니다.

두 번째 직원 bob

alice 와 같은 방법(키는 laptop 에서, gateway 는 서명만)으로 CN bob 의 클라이언트 인증서를 만들고, laptop 에 /etc/openvpn/client/bob.conf(tls-crypt 포함)를 써서 openvpn-client@bob 을 켜세요. laptop 에 tun 이 둘(alice·bob) 생기고 bob 의 tun 으로도 10.8.0.1 이 응답해야 합니다.

한 노트북에 두 사람을 띄운 것은 실습 편의입니다. 두 번째 tun(tun1)으로 핑하려면 ping -I tun1 10.8.0.1. 사내망 경로는 먼저 붙은 tun0 쪽에 하나만 들어갑니다.

bob 이 퇴사했다 — 인증서를 폐기하고 CRL 로 끊는다

./easyrsa revoke bob 과 ./easyrsa gen-crl 로 CRL 을 만들고 /etc/openvpn/server/crl.pem(권한 644)으로 놓은 뒤 서버 설정에 crl-verify crl.pem 을 더해 서버를 다시 켜세요. bob 은 다시 붙지 못하고(서버 기록에 certificate revoked), alice 는 계속 붙어 있어야 합니다. 거절을 확인했으면 laptop 의 openvpn-client@bob 을 끄세요(disable --now) — 퇴사자의 노트북은 회수했다고 칩니다.

폐기는 CA 의 장부(index.txt)에 R 을 적는 일이고, 서버가 그것을 알게 하는 것은 CRL 파일입니다. revoke 만 하고 gen-crl·배포를 빠뜨리면 bob 은 계속 들어옵니다. OpenVPN 은 crl.pem 이 바뀌면 다음 접속부터 다시 읽지만, 이미 붙어 있는 세션은 다음 재협상(기본 1시간)까지 남습니다 — 그래서 여기서는 서버를 다시 켭니다. bob 의 클라이언트를 켜 둔 채 두면 거절된 뒤에도 tun 장치(persist-tun)가 같은 대역으로 남습니다. 이 환경에서 bob 을 켜 둔 채로는 alice 가 다시 붙은 뒤 사내 위키가 열리지 않았고, 끄자 열렸습니다 — 같은 노트북의 두 터널이 한 대역을 나눠 쓰면 경로가 어느 쪽으로 들어갈지 장담할 수 없습니다.

CRL 에도 유통기한이 있다 — 다시 찍는 타이머

CRL 을 다시 찍어 서버 자리에 놓는 crl-renew.service(oneshot: easyrsa gen-crl → install -m 644 … /etc/openvpn/server/crl.pem)와 매주 도는 crl-renew.timer 를 만들어 켜고(enable --now), 서비스를 한 번 돌리세요. 그다음 /etc/openvpn/server/crl.pem 의 다음 갱신 시각(nextUpdate)을 /root/vpn/sec/crl-next.txt 에 한 줄로 적습니다.

easy-rsa 의 CRL 은 기본 180일 뒤 nextUpdate 가 지납니다. 그 뒤로 OpenVPN 은 만료된 CRL 을 이유로 모든 클라이언트를 거절합니다 — 폐기한 사람이 하나뿐이어도요. 그래서 CRL 은 한 번 찍고 끝이 아니라 주기적으로 다시 찍어야 합니다. openssl crl -in crl.pem -noout -nextupdate.

서버를 nobody 로 — 뚫려도 root 가 아니게

서버 설정에 user nobody, group nogroup 을 더하고 다시 켜세요. 서버의 openvpn 프로세스가 nobody 로 돌고, alice 는 다시 붙어야 합니다.

OpenVPN 은 root 로 시작해 tun 을 만들고 키를 읽은 뒤 권한을 내려놓습니다. 그 뒤로 다시 읽어야 하는 것(crl.pem, ccd 파일)은 nobody 가 읽을 수 있어야 하고, persist-key·persist-tun 이 있어야 재시작 때 다시 키를 읽거나 tun 을 다시 만들 필요가 없습니다. ps -o user=,args= -C openvpn.

alice 는 사내 위키(80)만 — 고정 주소와 포워딩 규칙

client-config-dir ccd 를 켜고 /etc/openvpn/server/ccd/alice 에 ifconfig-push 10.8.0.10 255.255.255.0 을 적어 alice 에게 고정 주소를 주세요. gateway 에 nft 표 inet vpn_acl(원본 /etc/vpn-acl.nft, 재부팅에도 남게)을 걸어 tun0 에서 들어온 것 중 10.8.0.10 → 10.10.0.10:80/tcp 와 이미 맺은 연결의 답만 넘기고 나머지는 버리세요. laptop 에서 http://10.10.0.10/whoami 는 10.8.0.10, nc -z -w3 10.10.0.10 22 는 실패해야 합니다.

VPN 이 붙었다는 것은 「사내망 아무 데나 가도 된다」 가 아닙니다. 주소를 사람에게 고정하면 방화벽 규칙을 사람 단위로 쓸 수 있습니다. ccd 파일 이름은 인증서 CN 이고, 서버가 nobody 로 돈 뒤에도 읽을 수 있어야 합니다(644). AWS Client VPN 의 인가 규칙이 같은 일을 그룹 단위로 합니다.