From 90d6569cfc6acd7dca6b0ea100a53acc9081c526 Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Mon, 13 Apr 2026 10:59:48 +1000 Subject: [PATCH] Add service exposure policy and external access retest script --- scripts/retest-external-access.sh | 37 +++++++++++++++ service-access-policy.md | 78 +++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100755 scripts/retest-external-access.sh create mode 100644 service-access-policy.md diff --git a/scripts/retest-external-access.sh b/scripts/retest-external-access.sh new file mode 100755 index 0000000..4c260b0 --- /dev/null +++ b/scripts/retest-external-access.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +PRIVATE_ADMIN_HOSTS=( + gotify.lan.ddnsgeek.com + grafana.lan.ddnsgeek.com + prometheus.lan.ddnsgeek.com + node-red.lan.ddnsgeek.com + traefik.lan.ddnsgeek.com + portainer.lan.ddnsgeek.com + influxdb.lan.ddnsgeek.com + kuma.lan.ddnsgeek.com + monitor-kuma.lan.ddnsgeek.com + edge.lan.ddnsgeek.com +) + +echo "[+] External retest started: $(date -u +'%Y-%m-%dT%H:%M:%SZ')" + +a11y_failures=0 +for host in "${PRIVATE_ADMIN_HOSTS[@]}"; do + code=$(curl -k -sS -o /dev/null -m 10 -w '%{http_code}' "https://${host}" || true) + if [[ "$code" == "000" || "$code" == "403" || "$code" == "401" ]]; then + echo "[PASS] ${host} blocked externally (code=${code})" + else + echo "[FAIL] ${host} appears reachable externally (code=${code})" + a11y_failures=$((a11y_failures + 1)) + fi +done + +echo +if [[ $a11y_failures -eq 0 ]]; then + echo "[PASS] Non-public services are inaccessible from this source path." + exit 0 +else + echo "[FAIL] ${a11y_failures} private-admin service(s) still externally reachable." + exit 1 +fi diff --git a/service-access-policy.md b/service-access-policy.md new file mode 100644 index 0000000..e09b067 --- /dev/null +++ b/service-access-policy.md @@ -0,0 +1,78 @@ +# Service Access Policy and External Exposure Hardening + +## 1) Service classification + +| Service/Host | Classification | Rationale | +|---|---|---| +| `auth.lan.ddnsgeek.com` | `authenticated-public` | Public identity/login entrypoint; internet-accessible but requires user authentication. | +| `nextcloud.lan.ddnsgeek.com` | `authenticated-public` | Internet-facing collaboration app that must remain reachable to authenticated users. | +| `passbolt.lan.ddnsgeek.com` | `authenticated-public` | Public password-management portal with strong authentication controls. | +| `gitea.lan.ddnsgeek.com` | `authenticated-public` | Public developer endpoint with account-based access. | +| `searxng.lan.ddnsgeek.com` | `public` | Intended anonymous/search access endpoint. | +| `familytree.lan.ddnsgeek.com` | `authenticated-public` | End-user app; externally reachable but login-protected. | +| `shifts.lan.ddnsgeek.com` | `authenticated-public` | End-user app; externally reachable but login-protected. | +| `stockfill.lan.ddnsgeek.com` | `authenticated-public` | End-user app; externally reachable but login-protected. | +| `gotify.lan.ddnsgeek.com` | `private-admin` | Admin/ops notification backend; should not be internet reachable. | +| `grafana.lan.ddnsgeek.com` | `private-admin` | Infrastructure admin/observability console. | +| `prometheus.lan.ddnsgeek.com` | `private-admin` | Monitoring datastore/query interface. | +| `node-red.lan.ddnsgeek.com` | `private-admin` | Automation runtime and flow editor. | +| `traefik.lan.ddnsgeek.com` | `private-admin` | Reverse-proxy admin/dashboard surface. | +| `portainer.lan.ddnsgeek.com` | `private-admin` | Container management plane. | +| `influxdb.lan.ddnsgeek.com` | `private-admin` | Metrics datastore admin/API surface. | +| `kuma.lan.ddnsgeek.com` | `private-admin` | Monitoring admin surface. | +| `monitor-kuma.lan.ddnsgeek.com` | `private-admin` | Monitoring admin surface. | +| `edge.lan.ddnsgeek.com` | `private-admin` | Edge/network administration plane. | + +## 2) Required controls for `private-admin` + +Apply **at least one** trusted-path control (preferably layered): + +- Private network only (no public DNS / no internet route). +- WireGuard/Tailscale/OpenVPN access gate. +- mTLS client certificate requirement at reverse proxy. +- Source IP allowlist at firewall and reverse proxy. + +Minimum target state for all `private-admin` hosts: + +- Public internet: connection refused/timeout, or immediate `403` for untrusted source. +- Trusted path (VPN/mTLS/allowlisted IP): normal authenticated access. + +## 3) Gateway auth hardening + +For all `public` and `authenticated-public` services: + +- Keep SSO and MFA enforcement at the identity gateway. +- Enforce lockout/backoff on `/login`, `/oauth/*`, `/auth/*`, `/api/auth/*`. +- Rate-limit by source IP + account identifier to deter credential stuffing. + +Suggested baseline: + +- Soft limit: `10 req/min` per IP for auth endpoints. +- Burst: `20`. +- Temporary block: `15 min` after repeated failures. +- Account lockout: `5-10` consecutive failed attempts (with secure unlock flow). + +## 4) WAF / reverse-proxy protections + +Deploy one of: + +- WAF managed rules for bot/credential-stuffing signatures. +- Reverse-proxy failed-auth throttling and tarpit/delay policy. + +Implement logging + alerting thresholds: + +- High failed-auth rate from one IP/CIDR. +- Password spray pattern across many usernames. +- Geo/ASN anomalies for sensitive apps. + +## 5) External re-test procedure + +Re-test from a non-trusted external network and record outcomes. + +Success criteria: + +- Every `private-admin` host is inaccessible without VPN/mTLS/allowlisted source. +- `public` and `authenticated-public` hosts remain reachable. +- Auth endpoints trigger rate-limit/lockout controls under failed-attempt simulation. + +Use `./scripts/retest-external-access.sh` for a repeatable external validation pass.