diff --git a/ansible/inventories/group_vars/pangolin/mini1.yaml b/ansible/inventories/group_vars/pangolin/mini1.yaml new file mode 100644 index 0000000..aceaaee --- /dev/null +++ b/ansible/inventories/group_vars/pangolin/mini1.yaml @@ -0,0 +1,5 @@ +--- +pangolin_dashboard_url: "https://pangolin.hassallab.it" +pangolin_base_domain: "hassallab.it" +pangolin_secret: "{{ pangolin_crypted_secret }}" +pangolin_admin_email: "hassallah@mail.com" 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/tasks/docker_pangolin.yaml b/ansible/playbooks/roles/pangolin/tasks/docker_pangolin.yaml new file mode 100644 index 0000000..c36a279 --- /dev/null +++ b/ansible/playbooks/roles/pangolin/tasks/docker_pangolin.yaml @@ -0,0 +1,99 @@ +- name: Ensure Pangolin directory exists + file: + path: /home/ubuntu/pangolin + state: directory + +- name: Ensure Pangolin config directory exists + file: + path: /home/ubuntu/pangolin/pangolin_config + state: directory + +- name: Template Pangolin config file + template: + src: pangolin_config.yml.j2 + dest: /home/ubuntu/pangolin/pangolin_config/config.yaml + +- name: Ensure Traefik config directory exists + file: + path: /home/ubuntu/pangolin/config/traefik + state: directory + +- name: Template Traefik config file + template: + src: traefik_config.yml.j2 + dest: /home/ubuntu/pangolin/config/traefik/traefik_config.yml + +- name: Template Traefik dynamic config file + template: + src: dynamic_config.yml.j2 + dest: /home/ubuntu/pangolin/config/traefik/dynamic_config.yml + +- name: Template docker-compose.yml for Pangolin + template: + src: docker-compose.yml.j2 + dest: /home/ubuntu/pangolin/docker-compose.yml + register: pangolin_compose_template + +- name: Check if Pangolin container is running + shell: docker ps --filter "name=pangolin" --filter "status=running" --format "{{'{{.Names}}'}}" + register: pangolin_running + changed_when: false + +- name: Check if Traefik container is running + shell: docker ps --filter "name=traefik" --filter "status=running" --format "{{'{{.Names}}'}}" + register: traefik_running + changed_when: false + +- name: Check if Gerbil container is running + 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 + 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 + 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) + 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 + shell: | + cd /home/ubuntu/pangolin + docker compose ps + register: docker_compose_status + +- name: Display Docker Compose service status + debug: + msg: "{{ docker_compose_status.stdout_lines }}" + +- name: "Assert that mandatory variables are defined and not default" + 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 + 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 }} \ No newline at end of file diff --git a/ansible/playbooks/roles/pangolin/tasks/main.yaml b/ansible/playbooks/roles/pangolin/tasks/main.yaml new file mode 100644 index 0000000..a40aeb3 --- /dev/null +++ b/ansible/playbooks/roles/pangolin/tasks/main.yaml @@ -0,0 +1,3 @@ +--- +include_tasks: prepare_node.yaml +include_tasks: docker_pangolin.yaml \ No newline at end of file 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..5a03ae2 --- /dev/null +++ b/ansible/playbooks/roles/pangolin/tasks/prepare_node.yaml @@ -0,0 +1,50 @@ +--- +- name: Update all packages + apt: + update_cache: yes + upgrade: dist + +- name: Install essential packages + apt: + name: + - net-tools + - curl + - wget + - htop + - vim + state: present + +- name: Add 1GB swap file + shell: | + fallocate -l 1G /swapfile + chmod 600 /swapfile + mkswap /swapfile + swapon /swapfile + args: + creates: /swapfile + +- name: Make swap persistent + lineinfile: + dest: /etc/fstab + line: '/swapfile none swap sw 0 0' + state: present + +- name: Configure iptables firewall rules + 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 + 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