--- # Gatus: health checks, status page and alerting for the whole estate. # # Built from source and run under systemd - upstream publishes no binaries, and # their Dockerfile shows the runtime needs nothing but the static binary and a # CA bundle. See roles/gatus/README.md. - name: Deploy Gatus on the observability host hosts: observability become: yes roles: - gatus # The dashboard is bound to loopback; Caddy publishes it. # # Auth is done HERE, at the edge, and not with Gatus's own `security.basic`. # Gatus's security middleware protects exactly four routes (api/api.go): # # /api/v1/endpoints/statuses # /api/v1/endpoints/:key/statuses # /api/v1/suites/statuses # /api/v1/suites/:key/statuses # # Everything else is registered on the UNPROTECTED router, including # /api/v1/config, every badge, and - the part that matters - # /api/v1/endpoints/:key/uptimes/:duration and .../response-times/:duration/history, # which return real per-endpoint data to anyone who can guess a key. Keys are # just "_". So Gatus's own auth makes the dashboard render empty # while leaving the data readable, which is worse than it looks. # # The one route that must NOT sit behind basic auth is the external-endpoint # push API. It authenticates with `Authorization: Bearer `, and basic # auth wants `Authorization: Basic <...>` - same header, two schemes, and the # push clients lose. It is not actually unauthenticated: the handler 401s on a # missing prefix, an empty token, or a token that does not match that endpoint's # own. Upstream's comment on the route says exactly that. - name: Publish the Gatus status page through Caddy hosts: observability become: yes tasks: - name: Require the dashboard credentials to be set ansible.builtin.assert: that: - gatus_dashboard_username is defined - gatus_dashboard_username | length > 0 - gatus_dashboard_password_hash is defined - gatus_dashboard_password_hash.startswith('$2') fail_msg: >- gatus_dashboard_username and gatus_dashboard_password_hash must be in the vault. Generate the hash on the observability host, which runs Caddy natively, so the bcrypt cost and format match what verifies it: caddy hash-password --plaintext 'your-password' then: ansible-vault edit group_vars/all/vault.yml - name: Configure the Caddy vhost for Gatus ansible.builtin.include_role: name: caddy_site vars: caddy_site_name: gatus caddy_site_domain: "{{ subdomains.gatus }}.{{ root_domain }}" # caddy_site_body rather than caddy_site_upstream + caddy_site_basic_auth, # because that pair applies auth to the whole site with no way to carve # out the push path. `handle` blocks are mutually exclusive and first # match wins, so the push API gets a route of its own. caddy_site_body: | @push { path /api/v1/endpoints/*/external method POST } # Push API: Bearer-authenticated by Gatus itself. No basic auth here, # or the Authorization header collides. handle @push { reverse_proxy 127.0.0.1:{{ gatus_port | default(8080) }} } # Everything else: the dashboard, the config endpoint, the badges and # the uptime/response-time history. handle { basic_auth { {{ gatus_dashboard_username }} {{ gatus_dashboard_password_hash }} } reverse_proxy 127.0.0.1:{{ gatus_port | default(8080) }} }