pangolin #1

Merged
sinibaldi merged 6 commits from pangolin into main 2026-07-24 16:10:13 +02:00
12 changed files with 403 additions and 9 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.DS_Store
.ansible
ansible/playbooks/wg_clients
collections

View File

@ -1,10 +1,15 @@
$ANSIBLE_VAULT;1.1;AES256
63363932663263346563646533323931303266346338383735666332363465373331653562663262
3837343464366635373863353833316532663539303938310a623664663134633033613839343835
32646630616663323035313563376133396563666434336561363930643832663763353136336135
6630656261323031610a626134346232383266343962393262353962613533373931656561396666
36616666383135663133646462333134653461326634336463353234383632396461663866383836
64316464626236663163323535643734633539653733353039336337626661373234356562653464
62613965616138633234396236656465316663616438326265356363623037663238656464633236
34663832343031663136643063356330366564393362643238386364373461396636363637346434
3937
35316234643930663465333664613362323061613838343333313331636366616163366162303165
3565383837343936306536633765616638646437356562630a396536396664303736303965383863
64383632386534316230366337633462613336666636633138626438336163373138616363323835
3137666361653932390a393432313565323939356232376430393762336136613762363036343961
36613462636630626430303636613130346533336534386165633232326238366134376233613466
35613338333035623237303336653137333133666234643466643166633636343234616430346130
31643238653833663761326262626536636537666162653863363934363165323134646564653736
65663334363037636266383930356664356531356664303261333539636462356533396634616130
63336431376532323630323531386437313639393639353332393061633764613030613261393862
36373738613136663165623834376462356139353932626364366164643038323665346639346462
30356166343639653632313833333565393436633733346136653538663235636333383333313533
31306634366535306464313738356662646130363662653062366635333837666464633330393038
31376561323264633063616530656661396331353165626135366462373663346333373131396535
6430326664303432393137353864366339366639643332356134

View File

@ -0,0 +1,14 @@
---
pangolin_dashboard_url: "https://pangolin.hassallab.it"
pangolin_base_domain: "hassallab.it"
pangolin_secret: "{{ pangolin_crypted_secret }}"
pangolin_admin_email: "hassallah@mail.com"
pangolin_gerbil_subnet_group: "10.42.0.0/16"
# Email (SMTP) settings
pangolin_email_smtp_host: "mail.com"
pangolin_email_smtp_port: 587
pangolin_email_smtp_user: "pango.lin@mail.com"
pangolin_email_smtp_pass: "{{ pangolin_mail_crypted_password }}"
pangolin_email_no_reply: "no-reply@mail.com"

View File

@ -0,0 +1,7 @@
---
- name: Install and configure Pangolin
hosts: pangolin
become: true
roles:
- geerlingguy.docker
- pangolin

View File

@ -0,0 +1,37 @@
---
# Derived Variables
pangolin_cors_origin: "{{ pangolin_dashboard_url }}"
# Logging
pangolin_log_level: "info"
# CORS
pangolin_cors_credentials: false
# Cert Resolver for Pangolin config
pangolin_cert_resolver: "letsencrypt"
# Gerbil (WireGuard) settings
pangolin_gerbil_start_port: 51820
pangolin_gerbil_use_subdomain: false
pangolin_gerbil_block_size: 24
pangolin_gerbil_site_block_size: 30
pangolin_gerbil_subnet_group: "100.89.137.0/20"
# Rate limits
pangolin_rate_limit_window_minutes: 1
pangolin_rate_limit_max_requests: 500
# Email (SMTP) settings
pangolin_email_smtp_host: "smtp.example.com"
pangolin_email_smtp_port: 587
pangolin_email_smtp_user: "user@example.com"
pangolin_email_smtp_pass: "changeme-email-password"
pangolin_email_no_reply: "no-reply@example.com"
# Feature flags
pangolin_flag_require_email_verification: false
pangolin_flag_disable_signup_without_invite: false
pangolin_flag_disable_user_create_org: false
pangolin_flag_allow_raw_resources: true
pangolin_flag_allow_base_domain_resources: true

View File

@ -0,0 +1,100 @@
---
- name: Ensure Pangolin directory exists
ansible.builtin.file:
path: /home/ubuntu/pangolin
state: directory
- name: Ensure Pangolin config directory exists
ansible.builtin.file:
path: /home/ubuntu/pangolin/pangolin_config
state: directory
- name: Template Pangolin config file
ansible.builtin.template:
src: pangolin_config.yml.j2
dest: /home/ubuntu/pangolin/pangolin_config/config.yaml
- name: Ensure Traefik config directory exists
ansible.builtin.file:
path: /home/ubuntu/pangolin/config/traefik
state: directory
- name: Template Traefik config file
ansible.builtin.template:
src: traefik_config.yml.j2
dest: /home/ubuntu/pangolin/config/traefik/traefik_config.yml
- name: Template Traefik dynamic config file
ansible.builtin.template:
src: dynamic_config.yml.j2
dest: /home/ubuntu/pangolin/config/traefik/dynamic_config.yml
- name: Template docker-compose.yml for Pangolin
ansible.builtin.template:
src: docker-compose.yml.j2
dest: /home/ubuntu/pangolin/docker-compose.yml
register: pangolin_compose_template
- name: Check if Pangolin container is running
ansible.builtin.shell: docker ps --filter "name=pangolin" --filter "status=running" --format "{{'{{.Names}}'}}"
register: pangolin_running
changed_when: false
- name: Check if Traefik container is running
ansible.builtin.shell: docker ps --filter "name=traefik" --filter "status=running" --format "{{'{{.Names}}'}}"
register: traefik_running
changed_when: false
- name: Check if Gerbil container is running
ansible.builtin.shell: docker ps --filter "name=gerbil" --filter "status=running" --format "{{'{{.Names}}'}}"
register: gerbil_running
changed_when: false
- name: Set fact if compose up is needed
ansible.builtin.set_fact:
pangolin_compose_needs_up: "{{ pangolin_compose_template.changed or (pangolin_running.stdout != 'pangolin') or (traefik_running.stdout != 'traefik') or (gerbil_running.stdout != 'gerbil') }}"
- name: Remove conflicting Pangolin containers if present
ansible.builtin.shell: |
docker rm -f pangolin || true
docker rm -f traefik || true
docker rm -f gerbil || true
when: pangolin_compose_needs_up
ignore_errors: true
- name: Start Pangolin and Gerbil with Docker Compose (force recreate if needed)
ansible.builtin.shell: |
cd /home/ubuntu/pangolin
docker compose up -d --force-recreate
when: pangolin_compose_needs_up
register: docker_compose_up
failed_when: docker_compose_up.rc != 0
- name: Check Docker Compose service status
ansible.builtin.shell: |
cd /home/ubuntu/pangolin
docker compose ps
register: docker_compose_status
- name: Display Docker Compose service status
ansible.builtin.debug:
msg: "{{ docker_compose_status.stdout_lines }}"
- name: "Assert that mandatory variables are defined and not default"
ansible.builtin.assert:
that:
- pangolin_dashboard_url != 'https://pangolin.example.com'
- pangolin_base_domain != 'example.com'
- pangolin_secret != 'changeme'
- pangolin_admin_email != 'admin@example.com'
fail_msg: "Please define all mandatory variables in group_vars/all/main.yml. Do not use the default example values."
success_msg: "All mandatory variables are properly defined."
- name: Display success message
ansible.builtin.debug:
msg: |
✅ Pangolin deployment is complete!
You can access your dashboard at: {{ pangolin_dashboard_url }}
Initial Setup URL: {{ pangolin_dashboard_url }}/auth/initial-setup
Admin User: {{ pangolin_admin_email }}

View File

@ -0,0 +1,3 @@
---
- ansible.builtin.include_tasks: prepare_node.yaml
- ansible.builtin.include_tasks: docker_pangolin.yaml

View File

@ -0,0 +1,50 @@
---
- name: Update all packages
ansible.builtin.apt:
update_cache: true
upgrade: dist
- name: Install essential packages
ansible.builtin.apt:
name:
- net-tools
- curl
- wget
- htop
- vim
state: present
- name: Add 1GB swap file
ansible.builtin.shell: |
fallocate -l 1G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
args:
creates: /swapfile
- name: Make swap persistent
ansible.builtin.lineinfile:
dest: /etc/fstab
line: '/swapfile none swap sw 0 0'
state: present
- name: Configure iptables firewall rules
ansible.builtin.iptables:
chain: INPUT
protocol: "{{ item.protocol }}"
destination_port: "{{ item.port }}"
jump: ACCEPT
action: insert
rule_num: 1
loop:
- { protocol: tcp, port: '80', description: 'HTTP for Lets Encrypt ACME challenge' }
- { protocol: tcp, port: '443', description: 'HTTPS for secure web traffic' }
- { protocol: udp, port: '51820', description: 'WireGuard VPN traffic' }
become: true
register: iptables_result
- name: Display iptables configuration status
ansible.builtin.debug:
msg: "Configured firewall rules for ports: 80 (HTTP), 443 (HTTPS), 51820 (WireGuard UDP)"
when: iptables_result is changed

View File

@ -0,0 +1,53 @@
services:
pangolin:
image: fosrl/pangolin:latest
container_name: pangolin
restart: unless-stopped
volumes:
- ./pangolin_config:/app/config
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
interval: "3s"
timeout: "3s"
retries: 15
gerbil:
image: fosrl/gerbil:1.0.0
container_name: gerbil
restart: unless-stopped
depends_on:
pangolin:
condition: service_healthy
command:
- --reachableAt=http://gerbil:3003
- --generateAndSaveKeyTo=/var/config/key
- --remoteConfig=http://pangolin:3001/api/v1/gerbil/get-config
- --reportBandwidthTo=http://pangolin:3001/api/v1/gerbil/receive-bandwidth
volumes:
- ./gerbil_config/:/var/config
cap_add:
- NET_ADMIN
- SYS_MODULE
ports:
- 51820:51820/udp
- 443:443 # Port for traefik because of the network_mode
- 80:80 # Port for traefik because of the network_mode
traefik:
image: traefik:v3.4.1
container_name: traefik
restart: unless-stopped
network_mode: service:gerbil # Ports appear on the gerbil service
depends_on:
pangolin:
condition: service_healthy
command:
- --configFile=/etc/traefik/traefik_config.yml
volumes:
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
networks:
default:
driver: bridge
name: pangolin

View File

@ -0,0 +1,53 @@
http:
middlewares:
redirect-to-https:
redirectScheme:
scheme: https
routers:
# HTTP to HTTPS redirect router
main-app-router-redirect:
rule: "Host(`{{ pangolin_dashboard_url | regex_replace('^https://', '') }}`)"
service: next-service
entryPoints:
- web
middlewares:
- redirect-to-https
# Next.js router (handles everything except API and WebSocket paths)
next-router:
rule: "Host(`{{ pangolin_dashboard_url | regex_replace('^https://', '') }}`) && !PathPrefix(`/api/v1`)"
service: next-service
entryPoints:
- websecure
tls:
certResolver: letsencrypt
# API router (handles /api/v1 paths)
api-router:
rule: "Host(`{{ pangolin_dashboard_url | regex_replace('^https://', '') }}`) && PathPrefix(`/api/v1`)"
service: api-service
entryPoints:
- websecure
tls:
certResolver: letsencrypt
# WebSocket router
ws-router:
rule: "Host(`{{ pangolin_dashboard_url | regex_replace('^https://', '') }}`)"
service: api-service
entryPoints:
- websecure
tls:
certResolver: letsencrypt
services:
next-service:
loadBalancer:
servers:
- url: "http://pangolin:3002"
api-service:
loadBalancer:
servers:
- url: "http://pangolin:3000"

View File

@ -0,0 +1,22 @@
# https://docs.fossorial.io/Pangolin/Configuration/config
app:
dashboard_url: "{{ pangolin_dashboard_url }}"
log_level: "{{ pangolin_log_level }}"
domains:
domain1:
base_domain: "{{ pangolin_base_domain }}"
cert_resolver: "{{ pangolin_cert_resolver }}"
server:
secret: "{{ pangolin_secret }}"
cors:
origins: ["{{ pangolin_cors_origin }}"]
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
allowed_headers: ["X-CSRF-Token", "Content-Type"]
credentials: {{ pangolin_cors_credentials | lower }}
gerbil:
start_port: {{ pangolin_gerbil_start_port }}
base_endpoint: "{{ pangolin_dashboard_url | regex_replace('^https://', '') }}" # Gerbil endpoint should be the FQDN, not the full URL

View File

@ -0,0 +1,49 @@
api:
insecure: true
dashboard: true
providers:
http:
endpoint: "http://pangolin:3001/api/v1/traefik-config"
pollInterval: "5s"
file:
filename: "/etc/traefik/dynamic_config.yml"
experimental:
plugins:
badger:
moduleName: "github.com/fosrl/badger"
version: "v1.2.0"
log:
level: "INFO"
format: "common"
maxSize: 100
maxBackups: 3
maxAge: 3
compress: true
certificatesResolvers:
letsencrypt:
acme:
httpChallenge:
entryPoint: web
email: {{ pangolin_admin_email }}
storage: "/letsencrypt/acme.json"
#caServer: "https://acme-staging-v02.api.letsencrypt.org/directory"
caServer: "https://acme-v02.api.letsencrypt.org/directory"
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
transport:
respondingTimeouts:
readTimeout: "30m"
http:
tls:
certResolver: "letsencrypt"
serversTransport:
insecureSkipVerify: true