Setup VPN Site-to-Site dengan WireGuard: Menghubungkan VPS Publik ke Server Lokal

Pernah gak sih punya server di belakang NAT yang pengen banget dites pake SSL beneran, tapi gak punya IP publik sendiri? Kalau iya, kamu gak sendirian aku juga ngalamin hal yang sama waktu belajar SSL.

Solusinya: WireGuard. Di catatan ini aku mau share proses menghubungkan dua server pake WireGuard satu VPS dengan IP publik (aku sebut Server Cloud), satu server di belakang NAT tanpa IP publik (aku sebut Server Lokal) biar Server Lokal bisa “numpang” IP publik dan domain milik Server Cloud. Kasus pemakaianku: testing SSL/TLS buat layanan di server lokal, tanpa harus punya IP publik sendiri.

Sebelum mulai: contoh di bawah pakai IP 203.0.x.x, dan key WireGuard ditulis sebagai placeholder kayak <PRIVATE_KEY_CLOUD>. Ganti semua itu dengan milikmu sendiri dan jangan pernah publikasikan private key WireGuard asli di blog atau tempat publik mana pun. Itu ibarat kunci rumah; kalau bocor, semua bisa masuk.

Topologi

Konfigurasi di Server Cloud

Cek Kompatibilitas Kernel & Firewall

Pertama, pastiin dulu kernel dan firewall-nya siap:

uname -r
modinfo wireguard 2>&1 | head -5
apt-cache policy wireguard
ufw status verbose
iptables -L -n

Kenapa perlu? Kernel modern (5.6 ke atas) udah punya modul WireGuard bawaan kalau modinfo wireguard nampilin detail modulnya, berarti gak perlu instalasi tambahan. apt-cache policy buat mastiin paketnya tersedia di repo. Dua command firewall terakhir buat mastiin gak ada rule yang bakal ngeblokir trafik WireGuard nanti.

Install WireGuard

apt update && apt install -y wireguard

Paket wireguard di Ubuntu otomatis narik wireguard-tools (berisi command wg dan wg-quick) sebagai dependency. Jadi gak perlu install manual satu-satu.

Generate Keypair

mkdir -p /etc/wireguard
umask 077
wg genkey | tee /etc/wireguard/private.key | wg pubkey > /etc/wireguard/public.key
cat /etc/wireguard/private.key
cat /etc/wireguard/public.key

Penjelasannya: umask 077 mastiin file yang dibuat setelahnya cuma bisa dibaca root penting, soalnya isinya private key. wg genkey bikin private key, terus dialirin (|) ke wg pubkey buat nurunin public key pasangannya.

Public key ini nanti dikirim ke Server Lokal aman kok buat dibagi. Tapi private key? Gak boleh keluar dari server ini ya guys.

Buat Konfigurasi wg0.conf

nano /etc/wireguard/wg0.conf
[Interface]
PrivateKey = 6xxxxxxxxxxxx8=
Address = 10.10.0.1/24
ListenPort = 51820
PostUp = iptables -I DOCKER-USER -i wg0 -j ACCEPT; iptables -I DOCKER-USER -o wg0 -j ACCEPT; iptables -t mangle -A FORWARD -o wg0 -p tcp --tcp-flag>
PostDown = iptables -D DOCKER-USER -i wg0 -j ACCEPT; iptables -D DOCKER-USER -o wg0 -j ACCEPT; iptables -t mangle -D FORWARD -o wg0 -p tcp --tcp-fl>

[Peer]
PublicKey = Axxxxxxxxxxxxxw=
AllowedIPs = 10.10.0.2/3
chmod 600 /etc/wireguard/wg0.conf

Penjelasan tiap barisnya:

BarisFungsi
Address = 10.10.0.1/24IP server ini di dalam jaringan VPN (bukan IP publik)
ListenPort = 51820Port UDP yang ditunggu buat koneksi masuk dari peer
PostUp (bagian DOCKER-USER)Kalau server juga jalanin Docker, Docker biasanya set default policy DROP buat trafik forward rule ini mastiin trafik lewat wg0 tetap diizinkan lewat chain DOCKER-USER (chain khusus yang gak ditimpa ulang sama Docker)
PostUp (bagian TCPMSS)Fix buat isu MTU (dibahas detail di Bagian 6) baris ini sebenernya ditambahkan belakangan setelah ketemu masalah, tapi aku tulis dari awal di sini biar config-nya langsung final
PostDownKebalikan dari PostUp bersihin rule iptables pas interface dimatikan, biar gak numpuk rule duplikat kalau restart berkali-kali

Catatan: bagian [Peer] buat Server Lokal belum ditambahkan di tahap ini baru bisa diisi setelah Server Lokal selesai generate key-nya sendiri.

Aktifkan IP Forwarding

echo "net.ipv4.ip_forward=1" > /etc/sysctl.d/99-wireguard.conf
sysctl -p /etc/sysctl.d/99-wireguard.conf

Tanpa ini, kernel Linux bakal nolak nerusin (forward) paket dari satu interface ke interface lain padahal itu persis yang dibutuhin biar trafik dari tunnel bisa diteruskan ke tujuan berikutnya (misal ke Docker atau ke internet).

Jalankan WireGuard

systemctl enable --now wg-quick@wg0
systemctl status wg-quick@wg0 --no-pager
wg show

enable --now sekaligus ngaktifin service buat sekarang dan saat boot berikutnya. wg show nampilin status interface — di tahap ini masih belum ada peer, karena belum dipasangkan.

Konfigurasi Di Server Lokal

Cek Versi & Kompatibilitas

cat /etc/rocky-release
uname -r
modinfo wireguard 2>&1 | head -5

Rocky Linux 9 ke atas udah punya modul kernel WireGuard bawaan (sama kayak Ubuntu modern). Kalau modinfo nampilin detail modul, berarti gak perlu instalasi modul tambahan lewat ELRepo/DKMS itu cuma dibutuhin di Rocky 8 ke bawah.

Install WireGuard Tools

dnf install -y epel-release
dnf install -y wireguard-tools

Beda dari Ubuntu, di keluarga RHEL (Rocky/CentOS/Alma) paket wireguard-tools ada di repo EPEL (Extra Packages for Enterprise Linux), bukan di repo bawaan makanya epel-release perlu diaktifin dulu.

Generate Keypair

mkdir -p /etc/wireguard
umask 077
wg genkey | tee /etc/wireguard/private.key | wg pubkey > /etc/wireguard/public.key
cat /etc/wireguard/private.key
cat /etc/wireguard/public.key

Logikanya sama persis kayak di Server Cloud. Public key hasil dari sini yang nanti ditempel ke [Peer] di config Server Cloud.

Buat Konfigurasi wg0.conf

Karena Server Lokal berperan sebagai client, config-nya langsung lengkap dengan [Peer] (nunjuk ke Server Cloud) dari awal:

[Interface]
PrivateKey = +Axxxxxxxxxxxxxxxxw=
Address = 10.10.0.2/24

[Peer]
PublicKey = vxxxxxxxxxxxxxxxxxc=
Endpoint = 104.64.x.x:51820
AllowedIPs = 10.10.0.0/24
PersistentKeepalive = 25

Penjelasan field yang beda dari Server Cloud:

FieldFungsi
EndpointAlamat IP publik + port Server Cloud yang dituju
AllowedIPs = 10.10.0.0/24Rentang IP yang dianggap “lewat tunnel ini” dibuat satu subnet penuh biar nanti kalau ada peer/server VPN lain di subnet yang sama, otomatis bisa dijangkau juga
PersistentKeepalive = 25Penting karena server ini di belakang NAT kirim paket kosong tiap 25 detik biar “lubang” NAT di router tetap kebuka. Tanpa ini, koneksi bisa putus sendiri setelah beberapa saat idle

Jalankan WireGuard

systemctl enable --now wg-quick@wg0
wg show
systemctl is-active firewalld

Cek juga firewalld (Rocky biasanya ngaktifin ini secara default, beda dari Ubuntu yang pake ufw) kalau statusnya active, mungkin perlu rule tambahan tergantung kebutuhan; kalau inactive, gak ada halangan tambahan.

Pairing: Daftarkan Server Lokal di Server Cloud

Balik ke Server Cloud, tambahkan [Peer] pake public key dari Server Lokal (hasil Bagian 2.3):

nano /etc/wireguard/wg0.conf

[Peer]
PublicKey = <PUBLIC_KEY_LOKAL>
AllowedIPs = 10.10.0.2/32
systemctl restart wg-quick@wg0
wg show

Kenapa AllowedIPs di sini beda dari sisi Server Lokal? Di sini dipake /32 (cuma 1 IP spesifik), bukan /24 karena dari sudut pandang Server Cloud, peer ini cuma mewakili satu host tunggal (Server Lokal itu sendiri), bukan gateway buat banyak host lain di baliknya.

Kalau berhasil, wg show bakal nampilin baris latest handshake: X seconds ago buat peer tersebut itu tandanya tunnel udah beneran konek, bukan cuma config-nya valid

Verifikasi Tunnel

Dari Server Cloud:

ping -c 4 10.10.0.2

Kalau dapet balasan (0% packet loss), tunnel-nya udah solid dan siap dipake buat trafik apa pun.

Expose Server Lokal ke Publik (Reverse Proxy Passthrough)

Ada 3 pendekatan buat “minjemin” IP publik Server Cloud ke Server Lokal:

OpsiCara kerjaKapan dipake
A. Reverse proxy biasaSSL di-terminate di proxy (pake cert milik proxy), terus diteruskan sebagai HTTP biasaWeb app umum, gak butuh cert asli dari backend
B. TCP/Stream passthroughProxy cuma nerusin paket mentah, SSL handshake terjadi langsung antara client dan Server LokalDipake di sini perlu cert asli dari Server Lokal (misal buat testing SSL)
C. iptables DNAT manualForward di level kernel, tanpa lewat proxy sama sekaliButuh forward protokol non-HTTP atau banyak port sekaligus

Panduan ini pake Opsi B, lewat Nginx Proxy Manager (NPM) yang udah jalan di Docker pada Server Cloud, dengan fitur “Streams”.

Pastikan Port Publik Belum Bentrok

ss -tulnp | grep 8443

Kosong = aman. (Contoh ini pake port 8443 biar gak bentrok sama port 443 yang udah dipake proxy host lain di NPM.)

Tambahkan Stream Lewat Web UI NPM

Konfigurasi Stream gak bisa lewat command line atau edit file config langsung NPM nyimpen konfigurasinya di database internal, jadi harus lewat web admin-nya:

  1. Buka http://<IP_PUBLIK_CLOUD>:81
  2. Login, buka menu Streams → Add Stream
    Isi:
    Incoming Port: 8443
    Forward Host: 10.10.0.2 (IP VPN Server Lokal)
    Forward Port: 443
  3. Centang TCP Forwarding
  4. Save

Port Gak Otomatis Kebuka ke Publik

Gejala: setelah Stream dibuat, coba akses https://<IP_PUBLIK_CLOUD>:8443 dari luar → Connection refused.

Penyebab: nambah Stream di UI NPM cuma bikin nginx di dalam container dengerin port itu kalau container Docker-nya sendiri belum “buka pintu” (-p / port mapping) dari host ke port tersebut, trafik dari luar gak akan pernah sampe.

Cara diagnosis:

docker port npm-app-1
docker inspect npm-app-1 --format='{{index .Config.Labels "com.docker.compose.project.config_files"}}'

Command pertama nunjukin port apa aja yang beneran ter-mapping ke host (kalau port baru gak muncul di sini, itu penyebabnya). Command kedua nyari lokasi file docker-compose.yml yang dipake buat container tersebut, lewat label yang otomatis ditambahkan Docker Compose.

Fix: edit file compose-nya, tambah port mapping, terus recreate container:

nano /srv/npm/docker-compose.yml

services:
  app:
    image: 'jc21/nginx-proxy-manager:2.15.1'
    restart: unless-stopped
    environment:
      TZ: "Australia/Brisbane"
    ports:
      - '80:80'
      - '81:81'
      - '443:443'
      - '8443:8443'
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt

cd /srv/npm
docker compose up -d
docker port npm-app-1

Catatan: recreate container ini bikin downtime singkat (beberapa detik) buat semua domain yang lewat NPM bukan cuma yang baru ditambahkan. Jadi kalau bisa, lakuin di jam sepi

MTU / PMTU Blackhole (Docker + WireGuard)

Gejala: setelah port udah kebuka dengan bener, koneksi dari luar gak lagi connection refused tapi malah macet total di tengah proses TLS handshake dan akhirnya timeout. Paket kecil (awal handshake) lewat, tapi begitu ada paket lebih gede (misal sertifikat SSL dikirim balik), macet total

Cara isolasi masalahnya bandingin test dari 2 titik berbeda:

# Dari HOST server cloud langsung (di luar container)
curl -vk --connect-timeout 5 https://10.10.0.2:443

# Dari DALAM container NPM
docker exec npm-app-1 curl -vk --connect-timeout 5 https://10.10.0.2:443

Kalau yang pertama berhasil penuh tapi yang kedua macet/timeout, itu bukti masalahnya bukan di tunnel WireGuard-nya (host bisa tembus sempurna), tapi spesifik di jalur container Docker → tunnel.

Penyebab: interface wg0 punya MTU 1420 (lebih kecil dari standar 1500 yang dipake jaringan Docker). Waktu ada paket gede yang perlu dipecah (fragmentasi) biar muat lewat wg0, mekanisme pemberitahuan otomatis (ICMP: Fragmentation Needed) sering gagal balik ke pengirim karena kehalang NAT Docker hasilnya paket gede itu ilang begitu aja tanpa pemberitahuan apa-apa (“blackhole”), bukan error yang jelas.

Fix: tambahkan rule TCPMSS --clamp-mss-to-pmtu ini bikin setiap koneksi TCP baru otomatis “sepakat” pake ukuran paket yang udah pas dari awal, gak perlu ngandelin mekanisme ICMP yang rawan gagal itu. (Rule ini udah dimasukin di config final Bagian 1.4, di baris PostUp/PostDown bagian TCPMSS.)

Kalau belum ditambahkan dari awal, gini cara update config yang udah jalan:

nano /etc/wireguard/wg0.conf

[Interface]
PrivateKey = <PRIVATE_KEY_CLOUD>
Address = 10.10.0.1/24
ListenPort = 51820
PostUp = iptables -I DOCKER-USER -i wg0 -j ACCEPT; iptables -I DOCKER-USER -o wg0 -j ACCEPT; iptables -t mangle -A FORWARD -o wg0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu; iptables -t mangle -A FORWARD -i wg0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
PostDown = iptables -D DOCKER-USER -i wg0 -j ACCEPT; iptables -D DOCKER-USER -o wg0 -j ACCEPT; iptables -t mangle -D FORWARD -o wg0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu; iptables -t mangle -D FORWARD -i wg0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

[Peer]
PublicKey = <PUBLIC_KEY_LOKAL>
AllowedIPs = 10.10.0.2/32

systemctl restart wg-quick@wg0
docker exec npm-app-1 curl -vk --connect-timeout 10 https://10.10.0.2:443

Kalau fix-nya berhasil, test curl dari dalam container sekarang bakal selesai penuh (dapet response HTTP/2 200), bukan lagi timeout.

Test End-to-End (dari Luar / Internet)

Dari komputer mana pun di internet (bukan dari kedua server):

curl -vk https://203.0.113.10:8443

Catatan buat pengguna Windows: di PowerShell, curl itu alias buat Invoke-WebRequest yang gak kenal flag -vk pake curl.exe secara eksplisit biar jalanin curl yang asli:

curl.exe -vk https://203.0.113.10:8443

Kalau berhasil, bakal keliatan HTTP/1.1 200 OK atau HTTP/2 200, dan isi halaman dari layanan di Server Lokal kebaca lengkap.
Kalau browser/curl nandain sertifikatnya “gak dipercaya” (gembok dicoret) itu wajar dan diduga, karena sertifikatnya self-signed.

Itu aman karena kepercayaan disisi sertifikatnya, bukan tanda tunnel/proxy-nya gagal. Kalau butuh sertifikat valid tanpa warning, bisa pasang Let’s Encrypt langsung di Server Lokal sekarang udah bisa, karena dia udah bisa dijangkau lewat domain publik

Leave a Reply

Your email address will not be published. Required fields are marked *