From bba3eb5012a0052b2320aba83b41756023c0908a Mon Sep 17 00:00:00 2001 From: Benjamin Brahmer Date: Sat, 10 Oct 2026 05:11:54 +0000 Subject: [PATCH 1/2] add new role for public grafana --- roles/grafana_public/defaults/main.yml | 14 +++ roles/grafana_public/handlers/main.yml | 10 +++ roles/grafana_public/tasks/main.yml | 86 +++++++++++++++++++ .../grafana_public/templates/grafana.conf.j2 | 33 +++++++ roles/grafana_public/templates/grafana.ini.j2 | 22 +++++ roles/meshviewer/defaults/main.yml | 1 + setup.yml | 1 + 7 files changed, 167 insertions(+) create mode 100644 roles/grafana_public/defaults/main.yml create mode 100644 roles/grafana_public/handlers/main.yml create mode 100644 roles/grafana_public/tasks/main.yml create mode 100644 roles/grafana_public/templates/grafana.conf.j2 create mode 100644 roles/grafana_public/templates/grafana.ini.j2 diff --git a/roles/grafana_public/defaults/main.yml b/roles/grafana_public/defaults/main.yml new file mode 100644 index 0000000..489981e --- /dev/null +++ b/roles/grafana_public/defaults/main.yml @@ -0,0 +1,14 @@ +--- +grafana_public_apt_key_url: https://apt.grafana.com/gpg.key +grafana_public_apt_keyring: /etc/apt/keyrings/grafana.gpg +grafana_public_apt_repository: "deb [signed-by={{ grafana_public_apt_keyring }}] https://apt.grafana.com stable main" + +grafana_public_domain: grafana.freifunk-suedholstein.de +grafana_public_root_url: "https://{{ grafana_public_domain }}/" +grafana_public_http_addr: 127.0.0.1 +grafana_public_http_port: 3000 +grafana_public_cert_file: "/etc/letsencrypt/live/{{ grafana_public_domain }}/fullchain.pem" +grafana_public_cert_key: "/etc/letsencrypt/live/{{ grafana_public_domain }}/privkey.pem" + +grafana_public_nginx_sites_available_dir: /etc/nginx/sites-available +grafana_public_nginx_sites_enabled_dir: /etc/nginx/sites-enabled diff --git a/roles/grafana_public/handlers/main.yml b/roles/grafana_public/handlers/main.yml new file mode 100644 index 0000000..fe08d77 --- /dev/null +++ b/roles/grafana_public/handlers/main.yml @@ -0,0 +1,10 @@ +--- +- name: Restart Grafana + ansible.builtin.systemd: + name: grafana-server + state: restarted + +- name: Reload nginx + ansible.builtin.systemd: + name: nginx + state: reloaded diff --git a/roles/grafana_public/tasks/main.yml b/roles/grafana_public/tasks/main.yml new file mode 100644 index 0000000..456461c --- /dev/null +++ b/roles/grafana_public/tasks/main.yml @@ -0,0 +1,86 @@ +--- +- name: Skip public Grafana on non-map hosts + ansible.builtin.meta: end_role + when: inventory_hostname != "map-ffsh" + +- name: Install prerequisites for Grafana repository + ansible.builtin.apt: + name: gnupg + state: present + +- name: Create APT keyrings directory + ansible.builtin.file: + path: /etc/apt/keyrings + state: directory + owner: root + group: root + mode: "0755" + +- name: Download Grafana repository signing key + ansible.builtin.get_url: + url: "{{ grafana_public_apt_key_url }}" + dest: /tmp/grafana-public.gpg.key + mode: "0644" + +- name: Convert Grafana repository signing key + ansible.builtin.command: + cmd: "gpg --dearmor --output {{ grafana_public_apt_keyring }} /tmp/grafana-public.gpg.key" + creates: "{{ grafana_public_apt_keyring }}" + +- name: Configure Grafana APT repository + ansible.builtin.apt_repository: + repo: "{{ grafana_public_apt_repository }}" + filename: grafana-public + state: present + update_cache: true + +- name: Install Grafana + ansible.builtin.apt: + name: grafana + state: present + +- name: Ensure Grafana service is enabled and running + ansible.builtin.systemd: + name: grafana-server + enabled: true + state: started + +- name: Configure Grafana public instance + ansible.builtin.template: + src: grafana.ini.j2 + dest: /etc/grafana/grafana.ini + owner: root + group: grafana + mode: "0640" + notify: Restart Grafana + +- name: Ensure nginx vhost directory exists + ansible.builtin.file: + path: "{{ grafana_public_nginx_sites_available_dir }}" + state: directory + owner: root + group: root + mode: "0755" + +- name: Configure Grafana nginx vhost + ansible.builtin.template: + src: grafana.conf.j2 + dest: "{{ grafana_public_nginx_sites_available_dir }}/{{ grafana_public_domain | replace('.', '-') }}.conf" + owner: root + group: root + mode: "0644" + notify: + - Reload nginx + +- name: Enable Grafana nginx vhost + ansible.builtin.file: + src: "{{ grafana_public_nginx_sites_available_dir }}/{{ grafana_public_domain | replace('.', '-') }}.conf" + dest: "{{ grafana_public_nginx_sites_enabled_dir }}/{{ grafana_public_domain | replace('.', '-') }}.conf" + state: link + force: true + +- name: Validate nginx configuration + ansible.builtin.command: nginx -t + changed_when: false + notify: + - Reload nginx diff --git a/roles/grafana_public/templates/grafana.conf.j2 b/roles/grafana_public/templates/grafana.conf.j2 new file mode 100644 index 0000000..15fbedc --- /dev/null +++ b/roles/grafana_public/templates/grafana.conf.j2 @@ -0,0 +1,33 @@ +server { + listen 80; + listen [::]:80; + + server_name {{ grafana_public_domain }}; + + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + + server_name {{ grafana_public_domain }}; + + ssl_certificate {{ grafana_public_cert_file }}; + ssl_certificate_key {{ grafana_public_cert_key }}; + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:50m; + ssl_session_tickets off; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers off; + + add_header Strict-Transport-Security "max-age=31536000" always; + + location / { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Host $http_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; + } +} diff --git a/roles/grafana_public/templates/grafana.ini.j2 b/roles/grafana_public/templates/grafana.ini.j2 new file mode 100644 index 0000000..bbb7213 --- /dev/null +++ b/roles/grafana_public/templates/grafana.ini.j2 @@ -0,0 +1,22 @@ +##################### Grafana Configuration ##################### +[paths] +[server] +http_addr = {{ grafana_public_http_addr }} +http_port = {{ grafana_public_http_port }} +domain = {{ grafana_public_domain }} +root_url = {{ grafana_public_root_url }} + +[database] +;type = sqlite3 +;path = grafana.db + +[security] +allow_embedding = true + +[users] +allow_sign_up = false + +[auth.anonymous] +enabled = true +org_name = Freifunk Südholstein +org_role = Viewer diff --git a/roles/meshviewer/defaults/main.yml b/roles/meshviewer/defaults/main.yml index 854b619..6d83d50 100644 --- a/roles/meshviewer/defaults/main.yml +++ b/roles/meshviewer/defaults/main.yml @@ -16,6 +16,7 @@ meshviewer_site_name: Freifunk Südholstein meshviewer_max_age: 21 meshviewer_node_zoom: 19 +# Grafana is proxied through the meshviewer nginx config to prevent cors issues meshviewer_grafana: url: /_grafana orgId: 1 diff --git a/setup.yml b/setup.yml index 51481a5..327ccda 100644 --- a/setup.yml +++ b/setup.yml @@ -104,6 +104,7 @@ - {role: firmware_release, tags: "firmware-release"} - {role: firmware_selector, tags: "firmware-selector"} - {role: website, tags: "website"} + - {role: grafana_public, tags: "grafana-public"} - {role: meshviewer, tags: "meshviewer"} post_tasks: From d28e00869bcc891fb4bf2d8ebd0b06d5febb9bea Mon Sep 17 00:00:00 2001 From: Benjamin Brahmer Date: Sat, 10 Oct 2026 07:02:43 +0000 Subject: [PATCH 2/2] resturcure the setup.yaml and hosts safer this way and add CSP for grafana --- .github/copilot/README.md | 9 ++- README.md | 23 +++++-- group_vars/{services.yml => services_web.yml} | 0 hosts.yml | 7 +- roles/firmware_release/tasks/main.yml | 4 -- roles/firmware_selector/tasks/main.yml | 4 -- roles/grafana_public/tasks/main.yml | 5 -- .../grafana_public/templates/grafana.conf.j2 | 1 + roles/meshviewer/tasks/main.yml | 5 -- setup.yml | 65 +++++++++++++++++-- 10 files changed, 93 insertions(+), 30 deletions(-) rename group_vars/{services.yml => services_web.yml} (100%) diff --git a/.github/copilot/README.md b/.github/copilot/README.md index c0100c6..5e519ea 100644 --- a/.github/copilot/README.md +++ b/.github/copilot/README.md @@ -17,7 +17,7 @@ This project provisions Freifunk Südholstein gateways and related services. The - README.md: deployment, usage, and vault guidance - setup.yml: main playbook with gateway, service, and monitoring roles -- hosts.yml: inventory for gateways, services, and monitoring hosts +- hosts.yml: inventory for gateways, split service groups, and monitoring hosts - group_vars/versions.yml: shared software versions - group_vars/secrets.yml: shared encrypted vars - host_vars/: host-specific secret and config values @@ -26,7 +26,8 @@ This project provisions Freifunk Südholstein gateways and related services. The ## Host groups - gateways: complete production router deployment -- services: lightweight service-only hosts +- services_map: map service host roles +- services_web: web service host roles - monitoring: Prometheus/Grafana stack ## Typical workflow @@ -34,7 +35,9 @@ This project provisions Freifunk Südholstein gateways and related services. The ```bash ansible-playbook --vault-id=fastd_key@prompt setup.yml ansible-playbook --vault-id=fastd_key@prompt setup.yml --limit gateways -ansible-playbook --vault-id=fastd_key@prompt setup.yml --limit services +ansible-playbook --vault-id=fastd_key@prompt setup.yml --limit "services_map:services_web" +ansible-playbook --vault-id=fastd_key@prompt setup.yml --limit services_map +ansible-playbook --vault-id=fastd_key@prompt setup.yml --limit services_web ansible-playbook --vault-id=fastd_key@prompt setup.yml --check ansible-lint ``` diff --git a/README.md b/README.md index bc5cceb..cd62fed 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Ansible [![Ansible Lint](https://github.com/ffsh/ansible/actions/workflows/ansible-lint.yml/badge.svg?branch=main)](https://github.com/ffsh/ansible/actions/workflows/ansible-lint.yml) -This repository holds the Ansible playbook to deploy a ffsh gateway in the standard configuration and to provision batman-only service hosts. +This repository holds the Ansible playbook to deploy a ffsh gateway in the standard configuration and to provision dedicated service hosts. It is based on https://docs.freifunk-suedholstein.de/de/1.5/gateway.html but changes were made where it was plausible. ## requirements @@ -48,7 +48,10 @@ That way Ansible will be able to automatically detect the right ssh key and conn ## hosts If you want to deploy to a new gateway you need to add it to the `hosts.yml` file in the repo, just compare it to brunsbach and add your own details. -If you want to add a batman-only service host, add it to the `services` group in `hosts.yml`. These hosts only receive the `batman` and `batctl` roles. +If you want to add a service host, place it in the dedicated service group in `hosts.yml`: + +- `services_map` for the map stack (`batman`, `batctl`, `grafana_public`, `meshviewer`) +- `services_web` for the web stack (`batman`, `batctl`, `website`, `firmware_release`, `firmware_selector`) The standalone DNS host belongs to the `dns` group. The DNS play only installs node_exporter and bind_exporter; it does not manage the existing BIND service, @@ -129,10 +132,22 @@ Run playbook on all gateways listed in `hosts`: ansible-playbook --vault-id=fastd_key@prompt setup.yml --limit gateways ``` -Run the batman-only service hosts (web-server map-server): +Run both service hosts: + +```bash +ansible-playbook --vault-id=fastd_key@prompt setup.yml --limit "services_map:services_web" +``` + +Run only the map service host: + +```bash +ansible-playbook --vault-id=fastd_key@prompt setup.yml --limit services_map +``` + +Run only the web service host: ```bash -ansible-playbook --vault-id=fastd_key@prompt setup.yml --limit services +ansible-playbook --vault-id=fastd_key@prompt setup.yml --limit services_web ``` Run the monitoring host: diff --git a/group_vars/services.yml b/group_vars/services_web.yml similarity index 100% rename from group_vars/services.yml rename to group_vars/services_web.yml diff --git a/hosts.yml b/hosts.yml index 78a0d84..c218f27 100644 --- a/hosts.yml +++ b/hosts.yml @@ -45,11 +45,16 @@ gateways: vars: ansible_user: root -services: +services_map: hosts: map-ffsh: ansible_ssh_host: 116.203.148.80 blackbox_http_url: https://map.freifunk-suedholstein.de/ + vars: + ansible_user: root + +services_web: + hosts: web-ffsh: ansible_ssh_host: 116.203.27.68 blackbox_http_url: https://freifunk-suedholstein.de/ diff --git a/roles/firmware_release/tasks/main.yml b/roles/firmware_release/tasks/main.yml index d19f860..53a984f 100644 --- a/roles/firmware_release/tasks/main.yml +++ b/roles/firmware_release/tasks/main.yml @@ -1,7 +1,3 @@ -- name: Skip firmware release helper on non-target hosts - ansible.builtin.meta: end_role - when: inventory_hostname != firmware_release_target_host - - name: Install firmware release helper dependencies ansible.builtin.apt: name: diff --git a/roles/firmware_selector/tasks/main.yml b/roles/firmware_selector/tasks/main.yml index 638861a..22bffd9 100644 --- a/roles/firmware_selector/tasks/main.yml +++ b/roles/firmware_selector/tasks/main.yml @@ -1,7 +1,3 @@ -- name: Skip firmware selector on non-target hosts - ansible.builtin.meta: end_role - when: inventory_hostname != firmware_selector_target_host - - name: Install firmware selector dependency ansible.builtin.apt: name: git diff --git a/roles/grafana_public/tasks/main.yml b/roles/grafana_public/tasks/main.yml index 456461c..139cb47 100644 --- a/roles/grafana_public/tasks/main.yml +++ b/roles/grafana_public/tasks/main.yml @@ -1,8 +1,3 @@ ---- -- name: Skip public Grafana on non-map hosts - ansible.builtin.meta: end_role - when: inventory_hostname != "map-ffsh" - - name: Install prerequisites for Grafana repository ansible.builtin.apt: name: gnupg diff --git a/roles/grafana_public/templates/grafana.conf.j2 b/roles/grafana_public/templates/grafana.conf.j2 index 15fbedc..8729430 100644 --- a/roles/grafana_public/templates/grafana.conf.j2 +++ b/roles/grafana_public/templates/grafana.conf.j2 @@ -22,6 +22,7 @@ server { ssl_prefer_server_ciphers off; add_header Strict-Transport-Security "max-age=31536000" always; + add_header Content-Security-Policy "frame-ancestors https://freifunk-suedholstein.de https://*.freifunk-suedholstein.de" always; location / { proxy_pass http://127.0.0.1:3000; diff --git a/roles/meshviewer/tasks/main.yml b/roles/meshviewer/tasks/main.yml index c7d72a7..f42513f 100644 --- a/roles/meshviewer/tasks/main.yml +++ b/roles/meshviewer/tasks/main.yml @@ -1,8 +1,3 @@ ---- -- name: Skip meshviewer on non-target hosts - ansible.builtin.meta: end_role - when: inventory_hostname != meshviewer_target_host - - name: Install meshviewer dependencies ansible.builtin.apt: name: diff --git a/setup.yml b/setup.yml index 327ccda..4e49d68 100644 --- a/setup.yml +++ b/setup.yml @@ -66,8 +66,67 @@ tags: - always -- name: Services Deployment - hosts: services +- name: Services Deployment (map) + hosts: services_map + serial: "{{ deploy_serial | default(1) }}" + vars_files: + - group_vars/versions.yml + vars: + users: + - username: root + pre_tasks: + - name: Load shared deployment secrets + ansible.builtin.include_vars: + file: group_vars/secrets.yml + name: vault_secrets + tags: + - always + + - name: Abort if target OS is not Ubuntu 24.04 + ansible.builtin.assert: + that: + - ansible_facts["distribution"] == "Ubuntu" + - ansible_facts["distribution_version"] == "24.04" + fail_msg: >- + Unsupported target OS: {{ ansible_facts["distribution"] }} {{ + ansible_facts["distribution_version"] }}. This playbook supports + Ubuntu 24.04 only. + success_msg: "Target OS check passed: Ubuntu 24.04" + tags: + - always + + - name: Update apt cache + ansible.builtin.apt: + update_cache: true + roles: + - {role: batman, tags: "batman-adv"} + - {role: batctl, tags: "batctl"} + - {role: grafana_public, tags: "grafana-public"} + - {role: meshviewer, tags: "meshviewer"} + + post_tasks: + - name: Check if reboot is required + ansible.builtin.stat: + path: /var/run/reboot-required + register: system_reboot_required + changed_when: false + tags: + - always + + - name: Reboot if required + ansible.builtin.reboot: + msg: Reboot triggered by Ansible because /var/run/reboot-required exists. + reboot_timeout: 900 + connect_timeout: 10 + post_reboot_delay: 15 + when: + - system_reboot_required.stat.exists + - not ansible_check_mode + tags: + - always + +- name: Services Deployment (web) + hosts: services_web serial: "{{ deploy_serial | default(1) }}" vars_files: - group_vars/versions.yml @@ -104,8 +163,6 @@ - {role: firmware_release, tags: "firmware-release"} - {role: firmware_selector, tags: "firmware-selector"} - {role: website, tags: "website"} - - {role: grafana_public, tags: "grafana-public"} - - {role: meshviewer, tags: "meshviewer"} post_tasks: - name: Check if reboot is required