diff --git a/.gitignore b/.gitignore index 1b4341d..fba9b79 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store +.ansible ansible/playbooks/wg_clients collections \ No newline at end of file diff --git a/ansible/inventories/group_vars/all/vault.yaml b/ansible/inventories/group_vars/all/vault.yaml index c7b0203..6178444 100644 --- a/ansible/inventories/group_vars/all/vault.yaml +++ b/ansible/inventories/group_vars/all/vault.yaml @@ -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 diff --git a/ansible/inventories/group_vars/pangolin/mini1.yaml b/ansible/inventories/group_vars/pangolin/mini1.yaml new file mode 100644 index 0000000..20664df --- /dev/null +++ b/ansible/inventories/group_vars/pangolin/mini1.yaml @@ -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" \ No newline at end of file diff --git a/ansible/playbooks/pangolin.yaml b/ansible/playbooks/pangolin.yaml new file mode 100644 index 0000000..b7858cf --- /dev/null +++ b/ansible/playbooks/pangolin.yaml @@ -0,0 +1,7 @@ +--- +- name: Install and configure Pangolin + hosts: pangolin + become: true + roles: + - geerlingguy.docker + - pangolin \ No newline at end of file diff --git a/ansible/playbooks/roles/pangolin/defaults/main.yaml b/ansible/playbooks/roles/pangolin/defaults/main.yaml new file mode 100644 index 0000000..29136b5 --- /dev/null +++ b/ansible/playbooks/roles/pangolin/defaults/main.yaml @@ -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 diff --git a/ansible/playbooks/roles/pangolin/tasks/docker_pangolin.yaml b/ansible/playbooks/roles/pangolin/tasks/docker_pangolin.yaml new file mode 100644 index 0000000..62f0799 --- /dev/null +++ b/ansible/playbooks/roles/pangolin/tasks/docker_pangolin.yaml @@ -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 }} diff --git a/ansible/playbooks/roles/pangolin/tasks/main.yaml b/ansible/playbooks/roles/pangolin/tasks/main.yaml new file mode 100644 index 0000000..2c34c0a --- /dev/null +++ b/ansible/playbooks/roles/pangolin/tasks/main.yaml @@ -0,0 +1,3 @@ +--- +- ansible.builtin.include_tasks: prepare_node.yaml +- ansible.builtin.include_tasks: docker_pangolin.yaml diff --git a/ansible/playbooks/roles/pangolin/tasks/prepare_node.yaml b/ansible/playbooks/roles/pangolin/tasks/prepare_node.yaml new file mode 100644 index 0000000..95581b0 --- /dev/null +++ b/ansible/playbooks/roles/pangolin/tasks/prepare_node.yaml @@ -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 diff --git a/ansible/playbooks/roles/pangolin/templates/docker-compose.yml.j2 b/ansible/playbooks/roles/pangolin/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..b0efb85 --- /dev/null +++ b/ansible/playbooks/roles/pangolin/templates/docker-compose.yml.j2 @@ -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 \ No newline at end of file diff --git a/ansible/playbooks/roles/pangolin/templates/dynamic_config.yml.j2 b/ansible/playbooks/roles/pangolin/templates/dynamic_config.yml.j2 new file mode 100644 index 0000000..33c3312 --- /dev/null +++ b/ansible/playbooks/roles/pangolin/templates/dynamic_config.yml.j2 @@ -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" \ No newline at end of file diff --git a/ansible/playbooks/roles/pangolin/templates/pangolin_config.yml.j2 b/ansible/playbooks/roles/pangolin/templates/pangolin_config.yml.j2 new file mode 100644 index 0000000..f1be62d --- /dev/null +++ b/ansible/playbooks/roles/pangolin/templates/pangolin_config.yml.j2 @@ -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 diff --git a/ansible/playbooks/roles/pangolin/templates/traefik_config.yml.j2 b/ansible/playbooks/roles/pangolin/templates/traefik_config.yml.j2 new file mode 100644 index 0000000..11356dc --- /dev/null +++ b/ansible/playbooks/roles/pangolin/templates/traefik_config.yml.j2 @@ -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 \ No newline at end of file