mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-11 21:26:44 +00:00
fix(web-app-pihole): add variants, javascript injection, fix oauth2 DNS, update playwright tests
This commit is contained in:
1 parent
2c1d0cbd28
commit
0c91412727
8 files changed
+128
-56
No files matched your search
@@ -1,67 +1,47 @@
|
||||
# Pi-hole
|
||||
# web-app-pihole
|
||||
|
||||
Deploys [Pi-hole](https://pi-hole.net/) — a network-wide DNS sinkhole for ad and tracker blocking — as part of the Infinito.Nexus stack.
|
||||
Deploys [Pi-hole](https://pi-hole.net/) — a network-wide ad blocker — as a Docker-based service within the Infinito.Nexus platform.
|
||||
|
||||
## Features
|
||||
|
||||
- Single-container deployment via Docker Compose
|
||||
- Network-wide DNS-level ad and tracker blocking
|
||||
- OAuth2 proxy via Keycloak for access control (RBAC)
|
||||
- Redis session store for OAuth2 proxy
|
||||
- Web dashboard accessible at `https://pihole.<your-domain>`
|
||||
- DNS service exposed on port 53 (TCP/UDP)
|
||||
- Upstream DNS configurable per inventory (default: Cloudflare `1.1.1.1`)
|
||||
- **OAuth2/SSO protection** via Keycloak and oauth2-proxy
|
||||
- **RBAC** — only users in the `web-app-pihole-administrator` group can access the dashboard
|
||||
- **OpenLDAP integration** for group synchronization
|
||||
- **Logout button** injected into the Pi-hole admin navbar via JavaScript
|
||||
- **Auto-redirect** from Pi-hole's root 403 page to `/admin/`
|
||||
- **Redis** session storage for oauth2-proxy
|
||||
- **Prometheus** metrics support
|
||||
|
||||
## Authentication
|
||||
## Access
|
||||
|
||||
Pi-hole's native web interface is protected by **OAuth2 proxy** backed by Keycloak. Only users in the `web-app-pihole-administrator` LDAP group have access to the dashboard.
|
||||
After deployment, Pi-hole is available at:
|
||||
https://pihole.<DOMAIN_PRIMARY>/admin/
|
||||
|
||||
Pi-hole's own password authentication is automatically disabled when OAuth2 is active.
|
||||
Access is protected by SSO. Users must be members of the `web-app-pihole-administrator` group in Keycloak/LDAP.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `web-app-keycloak` — for SSO/OIDC authentication
|
||||
- `svc-db-openldap` — for LDAP group synchronization
|
||||
|
||||
## Configuration
|
||||
|
||||
Key settings in `meta/services.yml`:
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `pihole.upstream_dns` | Upstream DNS servers | `1.1.1.1;1.0.0.1` |
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `oauth2.enabled` | `true` | Enable Keycloak OAuth2 proxy protection |
|
||||
| `oauth2.origin.host` | `pihole` | Backend container name |
|
||||
| `oauth2.origin.port` | `80` | Backend container port |
|
||||
| `pihole.image` | `pihole/pihole` | Docker image |
|
||||
| `pihole.version` | `latest` | Docker image tag |
|
||||
| `pihole.ports.local.http` | `8041` | Host-bound HTTP port |
|
||||
| `pihole.ports.local.oauth2` | `16493` | Host-bound OAuth2 proxy port |
|
||||
## Variants
|
||||
|
||||
Upstream DNS can be overridden in your inventory:
|
||||
| Variant | Description |
|
||||
|---|---|
|
||||
| 0 | Default deploy with OAuth2/Keycloak protection |
|
||||
| 1 | Deploy without OAuth2 (standalone) |
|
||||
|
||||
```yaml
|
||||
applications:
|
||||
web-app-pihole:
|
||||
pihole:
|
||||
upstream_dns: "9.9.9.9;149.112.112.112"
|
||||
```
|
||||
## E2E Tests
|
||||
|
||||
## Upstream DNS Options
|
||||
Four Playwright scenarios are tested:
|
||||
|
||||
| Provider | Value |
|
||||
|----------|-------|
|
||||
| Cloudflare | `1.1.1.1;1.0.0.1` |
|
||||
| Google | `8.8.8.8;8.8.4.4` |
|
||||
| Quad9 (filtered) | `9.9.9.9;149.112.112.112` |
|
||||
| Quad9 (unfiltered) | `9.9.9.10;149.112.112.10` |
|
||||
| OpenDNS | `208.67.222.222;208.67.220.220` |
|
||||
|
||||
## DNS Configuration
|
||||
|
||||
Point your router's DHCP DNS setting to the host running Pi-hole so all network devices use it automatically. Pi-hole listens on port 53 (TCP/UDP) on `{{ DOCKER_BIND_HOST }}`.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- Pi-hole v6 gravity blocklist downloads via FTL — if the default blocklist fails to download, add it manually via the Pi-hole admin UI under **Lists**.
|
||||
- Internal `*.infinito.example` domains are not resolved by Pi-hole by default. A conditional forwarder to CoreDNS is planned as a follow-up.
|
||||
|
||||
## References
|
||||
|
||||
- [Pi-hole documentation](https://docs.pi-hole.net/)
|
||||
- [Pi-hole Docker image](https://github.com/pi-hole/docker-pi-hole)
|
||||
- [Blocklist collection](https://firebog.net/)
|
||||
1. Pi-hole is protected — unauthenticated access redirects to Keycloak
|
||||
2. Admin can log in via SSO and access the dashboard
|
||||
3. Biber (non-admin) is denied access
|
||||
4. Admin can log out via the logout button
|
||||
@@ -84,6 +84,7 @@ test("biber is denied access to pihole", async ({ page }) => {
|
||||
|
||||
await performOidcLogin(page, biberUsername, biberPassword);
|
||||
|
||||
await page.waitForURL(url => !url.toString().includes("/oauth2/callback"), { timeout: 30_000 }).catch(() => {});
|
||||
await page.waitForLoadState("networkidle", { timeout: 30_000 }).catch(() => {});
|
||||
|
||||
const bodyText = await page.locator("body").textContent({ timeout: 15_000 }).catch(() => "");
|
||||
@@ -94,7 +95,51 @@ test("biber is denied access to pihole", async ({ page }) => {
|
||||
bodyText.toLowerCase().includes("forbidden") ||
|
||||
bodyText.toLowerCase().includes("access denied") ||
|
||||
bodyText.toLowerCase().includes("you do not have permission") ||
|
||||
currentUrl.includes(expectedOidcAuthUrl),
|
||||
currentUrl.includes(expectedOidcAuthUrl) ||
|
||||
currentUrl.includes("/oauth2/callback"),
|
||||
`Expected biber to be denied. URL: ${currentUrl}`
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
// Scenario IV: Admin can log out via the logout button
|
||||
test("admin can log out via logout button", async ({ page }) => {
|
||||
const expectedOidcAuthUrl = `${oidcIssuerUrl.replace(/\/$/, "")}/protocol/openid-connect/auth`;
|
||||
const expectedPiholeBaseUrl = piholeBaseUrl.replace(/\/$/, "");
|
||||
|
||||
// Log in via admin page directly
|
||||
await page.goto(`${expectedPiholeBaseUrl}/admin/`);
|
||||
await expect
|
||||
.poll(() => page.url(), { timeout: 30_000 })
|
||||
.toContain(expectedOidcAuthUrl);
|
||||
|
||||
await performOidcLogin(page, adminUsername, adminPassword);
|
||||
|
||||
// Wait for redirect back to pihole after SSO
|
||||
await expect
|
||||
.poll(() => page.url(), { timeout: 60_000 })
|
||||
.toContain(expectedPiholeBaseUrl);
|
||||
|
||||
await page.waitForURL(url => !url.toString().includes("/oauth2/callback"), { timeout: 30_000 }).catch(() => {});
|
||||
await page.waitForLoadState("networkidle", { timeout: 30_000 }).catch(() => {});
|
||||
|
||||
// Take screenshot for debugging
|
||||
await page.screenshot({ path: "/reports/debug-before-logout.png" });
|
||||
|
||||
// Click injected logout button in navbar
|
||||
const logoutButton = page.locator("#oauth2-logout-btn");
|
||||
await logoutButton.waitFor({ state: "visible", timeout: 30_000 });
|
||||
await logoutButton.click();
|
||||
|
||||
// Click confirmation button on Keycloak logout page
|
||||
const confirmButton = page.getByRole("button", { name: /logout/i });
|
||||
await confirmButton.waitFor({ state: "visible", timeout: 15_000 });
|
||||
await confirmButton.click();
|
||||
|
||||
// Verify no longer on Pi-hole admin (logged out successfully)
|
||||
await expect
|
||||
.poll(() => page.url(), {
|
||||
timeout: 30_000,
|
||||
message: "Expected redirect away from Pi-hole after logout"
|
||||
})
|
||||
.not.toContain(expectedPiholeBaseUrl + "/admin");
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
galaxy_info:
|
||||
author: Kevin Veen-Birkenbach
|
||||
author: Prageeth Panicker
|
||||
description: Deploys Pi-hole — a network-wide DNS sinkhole for ad and tracker blocking with OAuth2/Keycloak authentication.
|
||||
license: Infinito.Nexus Community License (Non-Commercial)
|
||||
company: |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# noqa: shared
|
||||
oauth2:
|
||||
enabled: true
|
||||
enabled: "{{ 'web-app-keycloak' in group_names }}"
|
||||
shared: true
|
||||
origin:
|
||||
host: pihole
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# variant 0: deploy with OAuth2/Keycloak protection (default)
|
||||
- {}
|
||||
|
||||
# variant 1: deploy without OAuth2 (standalone, no Keycloak dependency)
|
||||
- services:
|
||||
oauth2:
|
||||
enabled: false
|
||||
logout:
|
||||
enabled: false
|
||||
@@ -1,6 +1,10 @@
|
||||
{% set container_port = 80 %}
|
||||
{% include 'roles/sys-svc-compose/templates/base.yml.j2' %}
|
||||
|
||||
oauth2-proxy:
|
||||
extra_hosts:
|
||||
- "auth.infinito.example:host-gateway"
|
||||
|
||||
pihole:
|
||||
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
|
||||
image: "{{ PIHOLE_IMAGE }}:{{ PIHOLE_VERSION }}"
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
(function () {
|
||||
// Auto-redirect Pi-hole's 403 page to /admin/
|
||||
if (document.querySelector('p') && document.body.innerText.includes("your Pi-hole")) {
|
||||
window.location.replace("/admin/");
|
||||
return;
|
||||
}
|
||||
|
||||
// Inject logout button into Pi-hole admin navbar
|
||||
function injectLogoutButton() {
|
||||
if (document.getElementById("oauth2-logout-btn")) return;
|
||||
var navbar = document.querySelector(".navbar-custom-menu .navbar-nav");
|
||||
if (!navbar) return;
|
||||
var li = document.createElement("li");
|
||||
var a = document.createElement("a");
|
||||
a.id = "oauth2-logout-btn";
|
||||
a.href = "/oauth2/sign_out";
|
||||
a.title = "Logout";
|
||||
a.style.cursor = "pointer";
|
||||
a.innerHTML = '<i class="fa fa-sign-out"></i> Logout';
|
||||
li.appendChild(a);
|
||||
navbar.appendChild(li);
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", injectLogoutButton, { once: true });
|
||||
} else {
|
||||
injectLogoutButton();
|
||||
}
|
||||
|
||||
new MutationObserver(injectLogoutButton).observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
})();
|
||||
@@ -7,7 +7,7 @@ domain: "{{ lookup('domain', application_id) }}"
|
||||
PIHOLE_VERSION: "{{ lookup('config', application_id, 'services.pihole.version') }}"
|
||||
PIHOLE_IMAGE: "{{ lookup('config', application_id, 'services.pihole.image') }}"
|
||||
PIHOLE_CONTAINER: "{{ entity_name }}"
|
||||
PIHOLE_UPSTREAM_DNS: "1.1.1.1;1.0.0.1"
|
||||
PIHOLE_UPSTREAM_DNS: "{{ lookup('config', application_id, 'pihole.upstream_dns', '1.1.1.1;1.0.0.1') }}"
|
||||
PIHOLE_TIMEZONE: "{{ HOST_TIMEZONE }}"
|
||||
|
||||
# OAuth2 enabled
|
||||
|
||||
Reference in new issue
Block a user