홈랩 인트라넷을 구축해 보다
Shinwoo PARK
서론
나는 기존부터 쭉 홈랩과 셀프호스팅에 대한 일종의 낭만을 가지고 있었다.
비록 비용과 실용성의 문제로 서버와 셀프호스팅 서비스를 쭉 유지하지 못하더라도, 나는 꾸준히 여러 서비스들을 셀프호스팅 해서 써왔고, 나만의 서비스를 가진다는 것에 즐거움을 느꼈다.
AI가 나오게 되면서, AI와 함께 코딩하는 것에 익숙해지다 보니 필요한 것들을 내가 만드는 일이 잦아졌다. 그리고 그렇게 만들어진 것들을 내 리눅스 서버에 올려 돌리곤 해왔다.
한 가지 불편했던 점이라 하면, 내가 만드는 것들은 주로 나만 쓸 서비스들이었기 때문에, 매 서비스에 회원가입과 로그인 시스템을 넣는 것은 아무리 AI에게 부탁한다 해도 번거롭게 느껴졌으며, 특히 모든 서비스에 각각의 회원가입과 로그인 시스템을 넣다 보니 계정들을 관리하기가 번거롭게 느껴지기도 했다.
이런 문제점을 해결하고자 Cloudflare Tunnel과 Access를 써 왔다.
Cloudflare Tunnel은 nginx와 포트에 대한 부담을 줄여주었고, Access는 믿을 만한 게이트이자 통일화된 인증 시스템으로 내 문제점들을 해결해 주었다.
그러나 군대에 있으면서 인트라넷과 통일화된 로그인 시스템을 보고, 내 서버에도 Tunnel과 Access를 쓰는 대신 인트라넷 구축을 시도해보게 되었다.
설계

Tailscale
인트라넷이란, 조직 내부에서만 접근이 가능한 내부망을 말한다.
사설 VPN으로 구축하거나, 특정 백본을 이용하거나, 전용망을 깔아 구축하기도 한다.
내 인트라넷에서는 Tailscale이 핵심이 된다. Tailscale은 사설 VPN으로써, 인증된 사용자의 여러 기기에 Tailscale IP를 부여해 가상의 내부망을 형성하는 것이다.

DNS 셀프호스팅
거기에 더해 DNS를 셀프호스팅 하고자 했다.
물론, Tailscale에도 Magic DNS가 있다. 그러나 *.ts.net 도메인만 사용이 가능하고, 심지어 랜덤 뽑기로 도메인을 뽑아 써야 해 불편함이 있었다.
거기다, AdGuard Home을 셀프호스팅 하면 DNS와 동시에 광고 차단 효과도 볼 수 있어 추가적인 이득이 있었다.
다행히 인트라넷에 접속하는 기기에서 DNS를 변경해야 하는 부분은 Tailscale의 Custom DNS 설정으로 Tailscale에 연결하면 설정한 DNS을 사용하도록 할 수 있어 손쉽게 해결되었다.
Self-Signed Certificate
나는 인트라넷을 만들 때 나만의 TLD를 만들고자 했다. (memo.psw, auth.psw 처럼..)
보통 인증서는 신뢰된 CA(Certificate Authority)에서 서명을 받아 발급된다.
나만의 TLD를 만들 때 문제점은:
- CA는 존재하지 않는 TLD에 대해 인증서를 내어주지 않을 것이다
- 인트라넷 환경이므로, 공개 CA는 내 TLD 도메인에 대해 접근하거나 인증을 수행할 수 없다
때문에 자체 서명된 인증서가 필요했다.
그것을 위해, step-ca를 이용했다.
NGINX
마지막으로, 서비스 호스팅 서버에 남아 요청을 각 서비스에게 라우팅해 줄 리버스 프록시가 필요했다.
물론 다른 선택지도 많았지만, 내가 NGINX를 선택한 이유는 그저 손에 익었기 때문이다.
Tailscale 설치
모든 설치 및 설정에 앞서, 우선 가장 기초가 되는 사설 VPN을 위해, 내 모든 기기 및 서버에 Tailscale을 설치했다.
안드로이드, iOS, macOS, Windows 등 클라이언트가 될 기기는 웹사이트 또는 앱스토어에 들어가 Tailscale을 설치, 로그인하면 된다.
리눅스 headless 서버의 경우, 다음 명령어를 사용했다.
# 설치
curl -fsSL https://tailscale.com/install.sh | sh
# DNS 서버 — 자체 DNS를 사용할 것이므로
sudo tailscale up --accept-dns=false
# 서비스 서버 — 일반 연결
sudo tailscale up
# 각 서버의 Tailscale IP 확인
tailscale ip -4
tailscale ip -4로 나온 IP는 Tailscale에서 사용할 내부망 IP이다.
DNS 서버 설치 및 설정
1. Adguard Home 설치
Docker Compose로 간단하게 설치했다.
# /opt/adguardhome/docker-compose.yml
services:
adguard-home:
image: adguard/adguardhome:latest
container_name: adguard-home
volumes:
- ./work:/opt/adguardhome/work
- ./conf:/opt/adguardhome/conf
restart: unless-stopped
network_mode: host
이후 <tailscale-ip>:3000 으로 들어가보면 Adguard Home의 초기 설정 페이지가 나올 것이다.
초기 설정이 끝나면 :3000은 닫히고, :80으로 호스팅된다.
초기 설정에서는 관리자 계정을 설정하고, DNS 포트가 53인지 확인해야 한다.
DNS 설정
초기 설정(:3000)이 끝나고, 관리자 페이지(:80)로 넘어와서, 몇 가지 설정을 해야 한다.
1. DNS Rewrite 설정 (핵심)
Web UI → Filters → DNS Rewrites → Add DNS Rewrite
아래 DNS 레코드를 추가한다.
- *.psw → <서비스 서버 tailscale ip>
- ca.internal → <DNS 서버 tailscale ip>
psw는 내가 인트라넷에서 사용할 TLD이다. 만약 다른 TLD나 도메인을 사용하고 싶다면, 임의로 바꿔도 상관 없다.

ca.internal은 CA 요청용 도메인이다. 만약 다른 도메인으로 설정하고 싶다면 임의로 바꿔도 문제 없다. 단, 이후 설정에서도 바꾼 도메인을 일관성 있게 사용해야 한다.
2. 업스트림 DNS 설정
Settings → DNS settings → Upstream DNS servers
https://1.1.1.1/dns-query
https://8.8.8.8/dns-query
업스트림 DNS를 설정함으로서, 인트라넷의 도메인이 아닌 외부 도메인은 공개 DNS로 포워딩된다.
즉, 인트라넷에 연결한 상태에서도 외부 사이트에 접속할 수 있다는 뜻이다.
2. Tailscale DNS 설정
Tailscale Admin Console → DNS 탭에서 설정한다.
Nameservers → Add nameserver → Custom 클릭
Nameserver IP 항목에 DNS 서버의 tailscale ip를 입력하고 저장한다.

Override DNS Servers 옵션은 Tailscale에 연결된 기기의 기본 DNS를 무시한다.
이걸 켜지 않으면 로컬 DNS를 Tailscale DNS보다 먼저 사용하기 때문에, Adguard Home을 설치한 이유가 퇴색된다.
Adguard Home도 Upstream DNS가 설정되어 있기 때문에 다른 DNS와 다름없고, 안전하다.
3. 내부 CA 구축
step-ca 및 step CLI 설치
# (최신 버전 확인: github.com/smallstep/certificates)
# step-ca
wget https://dl.smallstep.com/gh-release/certificates/gh-release-header/v0.27.4/step-ca_linux_0.27.4_amd64.tar.gz
tar xzf step-ca_linux_0.27.4_amd64.tar.gz
sudo mv step-ca /usr/local/bin/
# step CLI
wget https://dl.smallstep.com/gh-release/cli/gh-release-header/v0.27.4/step_linux_0.27.4_amd64.tar.gz
tar xzf step_linux_0.27.4_amd64.tar.gz
sudo mv step_0.27.4/bin/step /usr/local/bin/
CA 초기화
step ca init \
--name "Home Internal CA" \
--dns "ca.internal" \
--dns "<DNS 서버 tailscale ip>" \
--address "<DNS 서버 tailscale ip>:8443" \
--provisioner "admin@internal" \
--acme
실행하면 다음 파일들이 생성된다.
~/.step/certs/root_ca.crt— 루트 CA 인증서~/.step/certs/intermediate_ca.crt— 중간 CA 인증서~/.step/secrets/— 개인키들
systemd 서비스 등록 및 활성화
sudo cat > /etc/systemd/system/step-ca.service << 'EOF'
[Unit]
Description=step-ca Internal CA
After=network.target
[Service]
User=step
ExecStartPre=/bin/sh -c 'until ip -4 addr show tailscale0 | grep -q <DNS 서버 tailscale ip>; do sleep 1; done'
ExecStart=/usr/local/bin/step-ca /home/step/.step/config/ca.json
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
sudo useradd --system --home /home/step --create-home step
sudo cp -r ~/.step /home/step/
sudo chown -R step:step /home/step/.step
sudo systemctl enable --now step-ca
ACME 프로비저너
다음 명령어로 프로비저너가 있는지 확인한다.
step ca provisioner list
# "acme" 프로비저너가 있어야 함
없다면, 다음 명령어로 추가한다.
step ca provisioner add acme --type acme
4. Nginx + 인증서 발급
이 작업은 서비스 서버에서 진행된다.
Nginx와 certbot 설치
sudo apt update && sudo apt install -y nginx certbot
Nginx 설정 만들기
내 서버는 기존에 nginx가 설치되어 돌아가고 있는 서비스가 있었으므로, 외부 노출 서비스와 인트라넷 서비스를 구분하기 위해 /etc/nginx/intra.d 디렉토리를 만들었다.
nginx 설정 파일은 다음과 같이 만든다.
server {
listen <서비스 서버 tailscale ip>:80;
server_name <도메인>;
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
default_type text/plain;
try_files $uri =404;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
// 첫 인증서 발급 이후 활성화
// listen <서비스 서버 tailscale ip>:443 ssl;
listen <서비스 서버 tailscale ip>:443;
server_name <도메인>;
// 첫 인증서 발급 이후 활성화
// ssl_certificate /etc/letsencrypt/live/env.psw/fullchain.pem;
// ssl_certificate_key /etc/letsencrypt/live/env.psw/privkey.pem;
location / {
proxy_pass http://127.0.0.1:<서비스 포트>;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
인증서 발급에는 HTTP-01, webroot 방식을 사용하므로 nginx에 인증 파일 제공을 위한 root 및 try_files를 넣는다.
첫 인증서 발급 전에는 인증서 파일이 없으므로, ssl을 적용하지 않고 443 포트로 열어만 둔다. 이후 인증서 발급이 끝나면 ssl을 적용한다.
인증서 발급
CA 초기화 과정에서 만들어진 루트 인증서를 서비스 서버로 가져왔다고 가정한다.
sudo certbot certonly \
--webroot -w /var/www/certbot \
--server https://ca.internal:8443/acme/acme/directory \
-d <도메인>
실행하면 인증서가 발급되고, certbot이 알아서 재발급까지 진행한다.
즉, 이 도메인에 대해 처음 발급받고 난 이후에는 재발급 등에 대해 신경쓸 필요가 없다.
다만, certbot이 인증서를 갱신하더라도 nginx가 다시 실행되지는 않아 오래된 인증서를 계속 사용하게 되는 문제가 있을 수 있다.
때문에, certbot에 hook을 추가해 인증서가 갱신된 후 nginx를 재시작하도록 해주어야 한다.
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
printf '#!/bin/sh\nset -e\nnginx -t\nsystemctl reload nginx\n' \
| sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
5. 루트 인증서 설치
이제 마지막 단계이다.
인증서를 발급했다고 한들, 우리가 직접 만든 CA는 기본적으로 신뢰되지 못한다.
루트 인증서 설치 단계 없이 tailscale에 연결하고 nginx으로 설정된 도메인에 들어가 보면, 브라우저는 신뢰되지 못한 인증서임을 불평할 것이다.
그렇기 때문에, 우리가 사용하는 기기에 “이 인증서를 발급한 기관은 신뢰할 수 있음”을 알려주어야 한다.
그것이 루트 인증서를 설치하는 단계이다.
아이폰(iOS)의 경우
- 루트 인증서를 내 휴대폰으로 옮긴다.
- 파일에서 루트 인증서를 찾아 열면 “프로필이 다운로드됨”이라는 메시지가 뜬다.
- 설정에 들어가 프로필을 설치한다.
- 설정 → 일반 → 정보 → 인증서 신뢰 설정
- "루트 인증서에 대한 완전한 신뢰 활성화" 섹션에서 Home Internal CA 토글을 ON
Android의 경우
- 루트 인증서를 내 휴대폰으로 옮긴다.
- 파일에서 루트 인증서를 찾아 열면 안드로이드가 인증서 설치를 감지해 설치 프롬프트 표시
참고: 인트라넷에 접속해야 하는 앱을 개발할 경우, 앱은 이 인증서 설정에 영향을 받지 않는다. 따라서, 앱 개발 과정에서 네트워크 보안 구성에 인증서를 추가해야 한다.
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>
Windows의 경우
- Win + R -> certmgr.msc
- 신뢰할 수 있는 루트 인증 기관 → 인증서 우클릭 → 모든 작업 → 가져오기
- 루트 인증서 파일 선택 → 신뢰할 수 있는 루트 인증 기관 저장소에 저장
또는 PowerShell로 한 번에 할 수도 있다.
Import-Certificate -FilePath "root_ca.crt" `
-CertStoreLocation Cert:\LocalMachine\Root
인트라넷을 구축해 보면서
처음 Tailscale을 알게 된 계기는, 원격 개발 시 로컬 개발 서버에 더 쉽게 접근하는 방법을 찾기 위해서였다.
그러나 Tailscale의 터널링과 MagicDNS는 내 원래 목적으로 쓰기에는 너무 복잡하고 번거롭게 느껴졌고, Tailscale이 어떤 서비스인지 자세히 알아보기도 전에 잊었었다.
그러나, 인트라넷을 만들고 싶다는 생각이 들자마자 Tailscale이 가장 먼저 떠올랐고, 결과적으로는 이것이 가장 좋은 선택지였다고 생각한다.
만약 Tailscale의 비용이 비쌌다면 아마 포기했을 것이다.
그러나 그룹에 들어올 수 있는 유저만 제한되고, 기기 등록은 무제한이라는 혜자스러운 정책 덕분에 인트라넷의 꿈을 이룰 수 있었다.
또, 네트워크에 대해서도 조금 더 공부해보고 싶다는 생각이 들었다.
인트라넷을 구축하면서도 네트워크의 흐름에 대해서만 이해했을 뿐, 구체적으로 Tailscale이 어떤 식으로 작동하고, 리눅스에서는 DNS가 어떻게 바뀌는지 등은 이해하지 못한 상태로 작업했다.
분명 Tailscale에 대해 더 깊게 알아가는 것이 컴퓨터 지식을 쌓아가는 데 있어 큰 역할을 할 것이라고 생각한다.