Merge pull request 'feat(squid): add SSL-bump caching forward-proxy deployment' (#4) from feat/squid-proxy into main

Reviewed-on: #4
This commit is contained in:
2026-06-22 21:33:34 +00:00
15 changed files with 1114 additions and 4 deletions
+4
View File
@@ -19,6 +19,10 @@ id_ed25519
id_ed25519_*
*.pem
# Squid TLS-interception CA -- generated on the host at deploy time, never
# committed (the private key can MITM any client that trusts it).
deployments/squid/ssl/
# ── Backups ─────────────────────────────────────────────────────────────────
*.tar.gz.age
*-backup-*.tar.gz*
+5 -3
View File
@@ -107,11 +107,13 @@ deployments/<name>/ # one folder per stack
| [`beszel`](deployments/beszel/) | Server monitoring hub. OIDC via pocket-id (post-deploy). | pocket-id (OIDC, optional) |
| [`headscale`](deployments/headscale/) | Self-hosted Tailscale control server, OIDC login. | pocket-id (OIDC) |
| [`webfinger`](deployments/webfinger/) | Serves `/.well-known/webfinger` for OIDC discovery; redirects the rest. | pocket-id (issuer) |
| [`squid`](deployments/squid/) | SSL-bump caching forward proxy — static-content cache + TLS interception via a local CA. **The exception: a forward proxy, not a Caddy/LE site.** | — |
| [`simplex`](deployments/simplex/) | SimpleX SMP + XFTP relay with Tor hidden services + encrypted backups. | globals/age-pubkey.txt |
## Conventions
- **Alpine + Docker Compose + Caddy/Let's Encrypt** across every stack.
- **Alpine + Docker Compose + Caddy/Let's Encrypt** across every stack (except
`squid`, which is a forward proxy with a local TLS-interception CA — no Caddy/LE).
- **`build.sh``deploy.sh`**: each stack's `build.sh` embeds its
`docker-compose.yml` / `Caddyfile` / `.env.example` into a single
self-contained `deploy.sh` (base64 tar.gz). That one file can be `scp`'d to a
@@ -140,8 +142,8 @@ instance user-data, and the host configures itself on first boot.
## Multi-OS notes
The host-provisioning scripts (`setup-host`, `harden-ssh`, `harden-jumphost`,
`sshuser`) and the four Docker stacks (pocket-id, beszel, headscale, webfinger)
run on Alpine, Debian, and Alma. Distro differences live in
`sshuser`) and the five Docker stacks (pocket-id, beszel, headscale, webfinger,
squid) run on Alpine, Debian, and Alma. Distro differences live in
[`scripts/oslib.sh`](scripts/oslib.sh) — package manager (`apk`/`apt`/`dnf`),
init system (OpenRC/systemd), sshd service name, the per-distro `sftp-server`
path, hostname, boot hooks, and the sshguard log source/backend.
+6 -1
View File
@@ -62,7 +62,7 @@ fi
. "$ROOT/scripts/lib.sh"
load_globals
DEPLOYMENTS=(pocket-id beszel headscale webfinger simplex)
DEPLOYMENTS=(pocket-id beszel headscale webfinger squid simplex)
SCRIPTS=(setup-host harden-ssh harden-jumphost sshuser auto-update)
# ----------------------------------------------------------------------------
@@ -108,6 +108,11 @@ ask_deployment_vars() {
ask ISSUER_URL "OIDC issuer URL (e.g. https://auth.example.com)"
ask REDIRECT_URL "Redirect target for other traffic (e.g. https://example.org)"
ask ACME_EMAIL "Let's Encrypt email" ;;
squid)
ask TRUSTED_CIDR "Trusted client CIDR(s) allowed to use the proxy (e.g. 100.64.0.0/10)"
ask BIND_ADDR "Host IP to bind the proxy on (blank = 0.0.0.0)" optional
ask CACHE_SIZE_MB "On-disk cache size in MB (blank = 5000)" optional
ask CACHE_ONLY_LISTED "Cache ONLY listed domains? (1=yes, blank=boost mode)" optional ;;
simplex)
ask DOMAIN "Apex domain (creates smp.DOMAIN, xftp.DOMAIN)"
ask ACME_EMAIL "Let's Encrypt email"
+41
View File
@@ -0,0 +1,41 @@
# Copy to .env and fill in. docker compose picks .env up automatically.
#
# Squid SSL-bump caching forward proxy. Unlike the other stacks there is NO
# public hostname / Let's Encrypt cert -- this is a forward proxy. The TLS
# interception CA is generated on first deploy (deploy.sh) and never overwritten.
# ─── Who may use the proxy ──────────────────────────────────────────────────
# Space-separated CIDR(s) allowed to connect (Squid http_access). This is the
# REAL access gate -- keep it tight. Examples: 100.64.0.0/10 (Tailscale CGNAT),
# 10.0.0.0/8, 192.168.0.0/16.
TRUSTED_CIDR=100.64.0.0/10
# Host interface/IP to publish the proxy on, and the host-side port. Pin
# BIND_ADDR to a trusted interface (e.g. your Tailscale IP) -- a published
# Docker port BYPASSES the host INPUT firewall, so 0.0.0.0 exposes the proxy to
# every reachable network. Use 0.0.0.0 only if TRUSTED_CIDR + upstream
# firewalling already cover you.
BIND_ADDR=0.0.0.0
PROXY_PORT=3128
# ─── Cache sizing ───────────────────────────────────────────────────────────
CACHE_SIZE_MB=5000 # on-disk cache budget (MB)
MAX_OBJECT_SIZE_MB=256 # largest single object to cache (raise for ISOs/images)
CACHE_MEM_MB=256 # in-memory hot cache (MB)
# ─── Cache scope ────────────────────────────────────────────────────────────
# 0 = boost mode (default): cache everything per normal HTTP rules, and
# force-cache the domains in cache-domains.txt / .regex with long TTLs.
# 1 = strict allowlist: store ONLY the listed domains; pass the rest through.
CACHE_ONLY_LISTED=0
# ─── TLS interception CA (generated on first deploy) ────────────────────────
CA_CN=Squid Proxy CA
CA_O=automations
CA_DAYS=3650
DYNAMIC_CERT_MEM_MB=8 # in-RAM cache of generated per-host leaf certs (MB)
# ─── Misc ───────────────────────────────────────────────────────────────────
VISIBLE_HOSTNAME=squid-proxy
# Local build tag for the image (built from ./Dockerfile).
SQUID_IMAGE_TAG=automations/squid:latest
+22
View File
@@ -0,0 +1,22 @@
# Minimal Squid image with SSL-bump.
#
# Alpine ships its `squid` package built `--with-openssl`, so ssl-bump,
# https_port and security_file_certgen are all compiled in -- no
# compile-from-source needed (Debian/Ubuntu, by contrast, build squid against
# GnuTLS and need the separate `squid-openssl` package). openssl is included so
# deploy.sh can mint the CA via this image without a host openssl dependency.
FROM alpine:3.21
RUN apk add --no-cache squid ca-certificates openssl tini \
&& update-ca-certificates
COPY squid.conf.tmpl /etc/squid/squid.conf.tmpl
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
# Explicit forward-proxy port. Caching/inspection happen here; clients set
# HTTP(S)_PROXY to this host:3128.
EXPOSE 3128
# tini reaps zombies and forwards signals so `docker stop` shuts squid cleanly.
ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/entrypoint.sh"]
+134
View File
@@ -0,0 +1,134 @@
# squid — SSL-bump caching forward proxy
A [Squid](https://www.squid-cache.org/) **forward proxy** that caches static
content to cut bandwidth across a fleet and **intercepts TLS** (SSL-bump) with a
locally-generated CA so HTTPS can be cached and inspected.
This is the one deployment that **breaks the repo's Caddy/Let's-Encrypt
convention on purpose**: it is not an inbound web service, has no public
hostname, and uses no ACME cert. Clients point `HTTP(S)_PROXY` at it and trust
its CA.
> ⚠️ **TLS interception — read first.** SSL-bump decrypts your clients' HTTPS.
> The CA private key it generates can impersonate **any** site to **any** client
> that trusts it. Only run this on networks and devices **you own and are
> authorized to inspect**. Keep `ssl/squid-ca-key.pem` secret. Cert-pinned and
> HSTS sites (banking, app stores) will break unless you **splice** them
> (passthrough, see below).
## What it does
- **Explicit forward proxy** on port `3128`. Clients set `http_proxy` /
`https_proxy` (transparent/intercepting mode is future work — see end).
- **SSL-bump**: peek at the TLS SNI → **splice** (passthrough, no decryption)
the domains in `splice-domains.txt`**bump** (decrypt) everything else,
minting per-host leaf certs on the fly from the local CA.
- **Hostname-targeted caching** with wildcards, and a storage gate that **never
caches HTML or dynamic content**.
## Deploy
**Via the launcher** (from a clone or the one-liner): pick `deploy: squid`, then
answer the prompts (trusted CIDR, etc.).
**Standalone** (self-contained `deploy.sh`, scp'd to a host):
```bash
TRUSTED_CIDR=100.64.0.0/10 BIND_ADDR=100.64.0.1 SKIP_PROMPTS=1 bash deploy.sh
```
**Fresh VM**: paste [`cloud-init.yml`](cloud-init.yml) as user-data (it hardens
SSH first, then deploys).
The deploy is idempotent. On first run it builds the local image, generates the
CA into `ssl/` (never overwritten), seeds `.env`, registers the port with the
host firewall if present, and brings the stack up.
## Point clients at it
```bash
export http_proxy=http://<host>:3128
export https_proxy=http://<host>:3128
```
Per-tool: apt → `Acquire::http(s)::Proxy "http://<host>:3128";`; dnf →
`proxy=http://<host>:3128` in `/etc/dnf/dnf.conf`; apk → `http_proxy` env or
`--proxy`.
**Trust the CA** (so bumped HTTPS validates) — distribute `ssl/squid-ca-cert.pem`:
| Client | Install |
|---|---|
| Debian/Ubuntu | `cp squid-ca-cert.pem /usr/local/share/ca-certificates/squid-ca.crt && update-ca-certificates` |
| Alpine | `cp squid-ca-cert.pem /usr/local/share/ca-certificates/squid-ca.crt && update-ca-certificates` |
| Alma/RHEL | `cp squid-ca-cert.pem /etc/pki/ca-trust/source/anchors/squid-ca.pem && update-ca-trust` |
Browsers, Java, and some language runtimes keep their own trust stores — import
there too if needed.
## Caching model
Two knobs and three lists, all in the stack dir; they are bind-mounted, so edit
then `docker compose restart`.
- **`cache-domains.txt`** — hostnames to cache hard (long TTL, force-cache past
`Cache-Control: private/no-store`). A **leading dot** is a subdomain wildcard:
`.ubuntu.com` matches `ubuntu.com` and every subdomain.
- **`cache-domains.regex`** — optional `dstdom_regex` patterns for wildcards
*inside* a label (e.g. `^mirror[0-9]+\.example\.com$`). Comments-only = disabled.
- **`CACHE_ONLY_LISTED`** (`.env`):
- `0` (default, *boost*): cache everything per normal HTTP rules, and
force-cache the listed domains aggressively.
- `1` (*strict allowlist*): store **only** the listed domains; pass the rest
through uncached.
**Never cached** (storage gate, applies even to boosted domains): HTML (by
`.html` extension and `text/html` content-type) and dynamic content (script
endpoints + query strings). Query strings are **exempt on boosted domains**, so
versioned static assets like `app.js?v=123` still cache there.
### splice ⇄ cache are mutually exclusive
A spliced domain is passed through encrypted — there is nothing to cache or
inspect. **Do not list the same domain in both** `splice-domains.txt` and
`cache-domains.txt`; splice wins.
## Security posture
- **Access control is Squid's `http_access`** (`TRUSTED_CIDR`), deny-by-default.
This matters because a **published Docker port bypasses the host `INPUT`
firewall** — so also pin **`BIND_ADDR`** to a trusted interface (e.g. your
Tailscale IP). The `/etc/firewall/ports.d/squid.rule` entry is belt-and-braces.
- **CA key** lives at `ssl/squid-ca-key.pem`, mode `0600` root, mounted
read-only; the container stages a squid-readable copy into `/run` (tmpfs) at
start. The key never enters the embedded archive and is git-ignored.
- **Upstream certs are validated** (`sslproxy_cert_error deny all`) — the proxy
won't silently launder a broken origin certificate to clients.
## Caching caveats (be realistic)
Even with bump, much of the web is `Cache-Control: private/no-store`, dynamic,
or personalized. The real wins are **distro packages** (apk/apt/dnf),
**container layers**, **OS updates**, and large static assets across many hosts
— which is what the default `cache-domains.txt` targets. It is not a blanket
"cache the whole internet."
## Files
| File | Purpose |
|---|---|
| `Dockerfile` | Minimal Alpine image (`apk add squid` — ssl-bump compiled in). |
| `entrypoint.sh` | Renders `squid.conf`, generates the cache policy from the lists, stages the CA, inits the cert DB/cache, starts squid. |
| `squid.conf.tmpl` | Static config (ports, bump policy, access control, cache sizing). |
| `cache-domains.txt` / `.regex` | Wildcard hostnames to cache hard. |
| `splice-domains.txt` | Domains to pass through without decrypting. |
| `docker-compose.yml` / `.env.example` | Stack definition + tunables. |
| `deploy.sh` / `build.sh` | Self-contained installer + archive embedder. |
| `cloud-init.yml` | Fresh-VM bootstrap (harden SSH, then deploy). |
## Future work
- **Transparent / intercepting mode** — add `intercept`/`tproxy` ports and an
iptables `REDIRECT` recipe for when the box is the gateway (clients need no
proxy config). Out of scope for v1.
- Upstream `cache_peer` chaining; access-log shipping / dashboard.
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
#
# build.sh -- (re)embed the loose deployment files into deploy.sh as a base64
# tar.gz payload after __ARCHIVE_BELOW__. Idempotent: strips any existing
# payload first.
#
# Run this after editing ANY embedded file below, then re-stage deploy.sh --
# the deployed stack uses the EMBEDDED copies, not the loose files.
set -euo pipefail
DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
SCRIPT="$DIR/deploy.sh"
MARKER="__ARCHIVE_BELOW__"
# The CA (ssl/), README, cloud-init, and the build/deploy scripts are NOT
# embedded -- the CA is generated on the host at deploy time.
FILES=(
docker-compose.yml
Dockerfile
entrypoint.sh
squid.conf.tmpl
splice-domains.txt
cache-domains.txt
cache-domains.regex
.env.example
)
[[ -f "$SCRIPT" ]] || { echo "deploy.sh not found at $SCRIPT" >&2; exit 1; }
for f in "${FILES[@]}"; do
[[ -f "$DIR/$f" ]] || { echo "Missing $DIR/$f" >&2; exit 1; }
done
PAYLOAD=$(tar -czf - -C "$DIR" "${FILES[@]}" | base64)
TMP=$(mktemp)
trap 'rm -f "$TMP"' EXIT
sed "/^${MARKER}\$/,\$d" "$SCRIPT" > "$TMP"
{
echo "$MARKER"
echo "$PAYLOAD"
} >> "$TMP"
mv "$TMP" "$SCRIPT"
chmod +x "$SCRIPT"
trap - EXIT
size=$(wc -c < "$SCRIPT")
echo "Built $SCRIPT (${size} bytes)"
+14
View File
@@ -0,0 +1,14 @@
# cache-domains.regex
#
# OPTIONAL. dstdom_regex patterns for glob-style hostnames that the leading-dot
# matching in cache-domains.txt can't express (e.g. a wildcard INSIDE a label).
# One POSIX extended regex per line, matched case-insensitively against the
# request hostname. Full-line "#" comments only.
#
# Leave this file with comments only to disable it -- the entrypoint skips it
# when there are no active entries (an empty ACL would otherwise error).
#
# Examples (uncomment / adapt):
# ^mirror[0-9]+\.example\.com$
# ^cdn-.*\.fastly\.net$
# ^.*\.pkg\.dev$
+42
View File
@@ -0,0 +1,42 @@
# cache-domains.txt
#
# Hostnames to cache aggressively (long TTL + force-cache, overriding
# Cache-Control: private/no-store). One entry per line; a LEADING DOT matches
# the domain and all subdomains (.ubuntu.com matches archive.ubuntu.com,
# security.ubuntu.com, ...). For wildcards INSIDE a label, use
# cache-domains.regex instead.
#
# These are BUMPED (decrypted) so HTTPS bodies can be cached -- do not also
# list them in splice-domains.txt. HTML and dynamic content are still skipped
# even here (see the storage gate); only static objects are stored.
#
# Defaults target high-bandwidth distro / package / container mirrors for a
# fleet of VMs. Trim or extend for your environment.
# ── Debian / Ubuntu ──
.ubuntu.com
.debian.org
.launchpad.net
# ── Alpine ──
.alpinelinux.org
# ── RHEL family (Alma / Rocky / Fedora / EPEL) ──
.almalinux.org
.rockylinux.org
.fedoraproject.org
.centos.org
# ── Container registries (layer blobs) ──
.docker.io
.docker.com
.ghcr.io
.quay.io
.gcr.io
.k8s.io
# ── Language package registries ──
.pypi.org
.pythonhosted.org
.npmjs.org
.crates.io
+41
View File
@@ -0,0 +1,41 @@
#cloud-config
#
# Squid SSL-bump caching proxy — harden SSH, then deploy, on a fresh host.
#
# Fill in REPO_URL and the values in the runcmd block, then paste this as the
# instance user-data. Unlike the web stacks this is a FORWARD proxy: no public
# DNS record or Let's Encrypt cert is needed, but clients must be able to reach
# TRUSTED_CIDR and must trust the CA this generates on first boot.
#
# Only deploy this on networks/devices you own and are authorized to inspect.
packages:
- git
runcmd:
- hostnamectl set-hostname squid || true
- |
set -e
REPO_URL=https://git.anomalous.dev/57_Wolve/automations.git
REPO_BRANCH=main
HARDEN_SSH=1 # harden SSH on this fresh VM (set 0 to skip)
SSH_PORT=22
ALLOWED_IP= # optional: whitelist your client IP in sshguard
git clone --depth 1 --branch "$REPO_BRANCH" "$REPO_URL" /opt/automations
cd /opt/automations
# Harden SSH first (PQ KEX, key-only auth, sshguard + deny-by-default
# firewall). The firewall it installs is what deploy.sh registers the proxy
# port with.
if [ "$HARDEN_SSH" = 1 ]; then
SSH_PORT="$SSH_PORT" ALLOWED_IP="$ALLOWED_IP" SKIP_PROMPTS=1 FORCE=1 \
bash scripts/harden-ssh.sh
fi
# Deploy the proxy. Set TRUSTED_CIDR to the network allowed to use it, and
# BIND_ADDR to a trusted interface (a published Docker port bypasses the
# host firewall, so this is the real exposure control).
TRUSTED_CIDR=100.64.0.0/10 \
BIND_ADDR=0.0.0.0 \
SKIP_PROMPTS=1 \
bash deployments/squid/deploy.sh
+453
View File
@@ -0,0 +1,453 @@
#!/usr/bin/env bash
#
# deploy.sh -- deploy the Squid SSL-bump caching forward proxy.
#
# What this does:
# 1. Installs docker + compose if missing.
# 2. Lays down the stack files in $STACK_DIR and builds the local image.
# 3. Generates the TLS interception CA on first run (never overwritten).
# 4. Generates .env on first run; existing .env is never overwritten.
# 5. Prompts for required values not preset (TRUSTED_CIDR).
# 6. Registers the proxy port with the host firewall if present.
# 7. Brings the stack up and waits for health.
#
# Role: this is a FORWARD proxy, not an inbound web service -- there is no
# public hostname or Let's Encrypt cert. Clients set HTTP(S)_PROXY to this host
# and trust the generated CA. Only run it on networks/devices you own.
#
# Idempotent: re-run to apply config changes / rebuild the image.
#
# Self-contained: the compose file, Dockerfile, entrypoint, config template and
# domain lists are embedded as a base64 tar.gz at the bottom of this file.
# Rebuild with build.sh after editing the loose source files.
#
# Usage:
# bash deploy.sh # interactive prompts
# TRUSTED_CIDR=10.0.0.0/8 BIND_ADDR=100.64.0.1 \
# SKIP_PROMPTS=1 bash deploy.sh # non-interactive
# STACK_DIR=/opt/squid bash deploy.sh
# SKIP_DOCKER_INSTALL=1 bash deploy.sh
set -euo pipefail
: "${STACK_DIR:=/srv/squid}"
: "${SKIP_DOCKER_INSTALL:=0}"
: "${FORCE:=0}"
: "${SKIP_PROMPTS:=0}" # non-interactive: require values via env, no prompts
[[ "$SKIP_PROMPTS" == "1" ]] && FORCE=1
: "${TRUSTED_CIDR:=}"
: "${BIND_ADDR:=}"
: "${PROXY_PORT:=3128}"
: "${CACHE_SIZE_MB:=}"
: "${CACHE_ONLY_LISTED:=}"
: "${CA_CN:=Squid Proxy CA}"
: "${CA_O:=automations}"
: "${CA_DAYS:=3650}"
: "${SQUID_IMAGE_TAG:=automations/squid:latest}"
log() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*" >&2; }
die() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; exit 1; }
[[ $EUID -eq 0 ]] || die "Run as root."
# ---------------------------------------------------------------------------
# OS detection + Docker install (Alpine / Debian / Alma). This deploy.sh is
# self-contained (scp'd standalone), so the OS logic is inlined here instead
# of sourced from scripts/oslib.sh.
# ---------------------------------------------------------------------------
osfam() {
local id="" like=""
if [[ -r /etc/os-release ]]; then
id="$(. /etc/os-release 2>/dev/null && echo "${ID:-}")"
like="$(. /etc/os-release 2>/dev/null && echo "${ID_LIKE:-}")"
fi
case " $id $like " in
*" alpine "*) echo alpine ;;
*" debian "*|*" ubuntu "*) echo debian ;;
*" rhel "*|*" fedora "*|*" centos "*) echo rhel ;;
*) echo "${id:-unknown}" ;;
esac
}
install_docker() {
if command -v docker >/dev/null 2>&1; then
log "Docker already installed: $(docker --version)"
else
log "Installing Docker (OS: $(osfam))..."
case "$(osfam)" in
alpine) apk add -q docker docker-cli-compose openrc ;;
debian|rhel) command -v curl >/dev/null 2>&1 || \
{ command -v apt-get >/dev/null 2>&1 && apt-get install -y -qq curl; } || \
{ command -v dnf >/dev/null 2>&1 && dnf install -y -q curl; }
curl -fsSL https://get.docker.com | sh ;;
*) die "Unsupported OS for auto Docker install. Set SKIP_DOCKER_INSTALL=1 and install Docker yourself." ;;
esac
fi
if command -v rc-update >/dev/null 2>&1; then
rc-update add docker default >/dev/null 2>&1 || true
rc-service docker status >/dev/null 2>&1 || rc-service docker start
elif command -v systemctl >/dev/null 2>&1; then
systemctl enable --now docker >/dev/null 2>&1 || systemctl start docker || true
fi
}
open_proxy_port() {
# Register the proxy port. Prefer the host firewall (harden-firewall.sh)
# when present; else ufw/firewalld if active.
#
# NOTE: a Docker-published port reaches the host through nat/FORWARD and
# BYPASSES the INPUT firewall, so this is belt-and-braces -- the real access
# gate is Squid's http_access (TRUSTED_CIDR) plus pinning BIND_ADDR to a
# trusted interface. The ports.d format is port-only (no source CIDR), so
# restrict the source via BIND_ADDR / upstream firewalling, not here.
local port="${PROXY_PORT:-3128}"
if [[ -d /etc/firewall/ports.d && -x /usr/local/sbin/firewall-apply ]]; then
log "Registering ${port}/tcp with host firewall..."
printf '%s/tcp\n' "$port" > /etc/firewall/ports.d/squid.rule
/usr/local/sbin/firewall-apply
elif command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q '^Status: active'; then
log "ufw active -- allowing ${port}/tcp..."
ufw allow "${port}/tcp" >/dev/null
elif command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then
log "firewalld active -- allowing ${port}/tcp..."
firewall-cmd -q --add-port="${port}/tcp" --permanent
firewall-cmd -q --reload
fi
}
# ----------------------------------------------------------------------------
# Extract embedded archive
# ----------------------------------------------------------------------------
SCRIPT_DIR=$(mktemp -d -t squid-deploy.XXXXXX)
trap 'rm -rf "$SCRIPT_DIR"' EXIT
extract_archive() {
grep -a -A 9999999 '^__ARCHIVE_BELOW__$' "$0" \
| tail -n +2 \
| base64 -d \
| tar -xz -C "$SCRIPT_DIR"
}
if grep -q -a '^__ARCHIVE_BELOW__$' "$0"; then
log "Extracting embedded deployment files..."
extract_archive
else
die "No embedded archive found. Run build.sh to embed deployment files."
fi
EMBEDDED=(docker-compose.yml Dockerfile entrypoint.sh squid.conf.tmpl
splice-domains.txt cache-domains.txt cache-domains.regex .env.example)
for f in "${EMBEDDED[@]}"; do
[[ -f "$SCRIPT_DIR/$f" ]] || die "Embedded archive missing $f"
done
# ----------------------------------------------------------------------------
# Prompt for required vars
# ----------------------------------------------------------------------------
prompt() {
local varname="$1" message="$2"
local -n ref="$varname"
if [[ -z "${ref:-}" ]]; then
[[ "$SKIP_PROMPTS" == "1" ]] && die "$varname required (set it in the environment; running with SKIP_PROMPTS=1)."
read -r -p "$message: " ref
[[ -n "$ref" ]] || die "$varname required."
fi
}
prompt TRUSTED_CIDR "Trusted client CIDR(s) allowed to use the proxy (e.g. 100.64.0.0/10)"
# ----------------------------------------------------------------------------
# Docker + firewall
# ----------------------------------------------------------------------------
if [[ "$SKIP_DOCKER_INSTALL" != "1" ]]; then
install_docker
fi
open_proxy_port
# ----------------------------------------------------------------------------
# Stack directory + files
# ----------------------------------------------------------------------------
log "Setting up $STACK_DIR..."
install -d -m 0750 "$STACK_DIR"
install -m 0644 "$SCRIPT_DIR/docker-compose.yml" "$STACK_DIR/docker-compose.yml"
install -m 0644 "$SCRIPT_DIR/Dockerfile" "$STACK_DIR/Dockerfile"
install -m 0755 "$SCRIPT_DIR/entrypoint.sh" "$STACK_DIR/entrypoint.sh"
install -m 0644 "$SCRIPT_DIR/squid.conf.tmpl" "$STACK_DIR/squid.conf.tmpl"
install -m 0644 "$SCRIPT_DIR/splice-domains.txt" "$STACK_DIR/splice-domains.txt"
install -m 0644 "$SCRIPT_DIR/cache-domains.txt" "$STACK_DIR/cache-domains.txt"
install -m 0644 "$SCRIPT_DIR/cache-domains.regex" "$STACK_DIR/cache-domains.regex"
ENV_FILE="$STACK_DIR/.env"
set_env() { # <KEY> <value>: update KEY in .env, or append if absent
local key="$1" val="$2" esc
esc=${val//\\/\\\\}; esc=${esc//|/\\|}; esc=${esc//&/\\&}
if grep -qE "^${key}=" "$ENV_FILE"; then
sed -i -e "s|^${key}=.*|${key}=${esc}|" "$ENV_FILE"
else
printf '%s=%s\n' "$key" "$val" >> "$ENV_FILE"
fi
}
if [[ ! -f "$ENV_FILE" ]]; then
log "Seeding $ENV_FILE..."
install -m 0600 "$SCRIPT_DIR/.env.example" "$ENV_FILE"
set_env TRUSTED_CIDR "$TRUSTED_CIDR"
if [[ -n "${BIND_ADDR:-}" ]]; then set_env BIND_ADDR "$BIND_ADDR"; fi
if [[ -n "${PROXY_PORT:-}" ]]; then set_env PROXY_PORT "$PROXY_PORT"; fi
if [[ -n "${CACHE_SIZE_MB:-}" ]]; then set_env CACHE_SIZE_MB "$CACHE_SIZE_MB"; fi
if [[ -n "${CACHE_ONLY_LISTED:-}" ]]; then set_env CACHE_ONLY_LISTED "$CACHE_ONLY_LISTED"; fi
set_env CA_CN "$CA_CN"
set_env CA_O "$CA_O"
set_env SQUID_IMAGE_TAG "$SQUID_IMAGE_TAG"
else
log ".env exists; leaving it alone."
fi
# Validate required value landed.
grep -E "^TRUSTED_CIDR=.+$" "$ENV_FILE" >/dev/null || die "TRUSTED_CIDR missing in $ENV_FILE."
# Use the image tag actually recorded in .env (keeps build + CA-gen in sync).
IMAGE=$(grep -E '^SQUID_IMAGE_TAG=' "$ENV_FILE" | head -n1 | cut -d= -f2-)
IMAGE=${IMAGE:-$SQUID_IMAGE_TAG}
# ----------------------------------------------------------------------------
# Confirm (interception is a big deal)
# ----------------------------------------------------------------------------
if [[ "$FORCE" != "1" ]]; then
cat <<EOF
About to build the Squid image and start an SSL-bump forward proxy from
$STACK_DIR.
This generates a local CA and INTERCEPTS TLS for every client that trusts it
(except spliced domains). Only do this on networks/devices you own and are
authorized to inspect. Clients reach it at: http://<this-host>:${PROXY_PORT}
Continue? [y/N]
EOF
read -r ans
[[ "${ans,,}" == "y" || "${ans,,}" == "yes" ]] || { warn "Aborted."; exit 0; }
fi
cd "$STACK_DIR"
# ----------------------------------------------------------------------------
# Build image, then generate the CA (needs the built image's openssl)
# ----------------------------------------------------------------------------
log "Building image $IMAGE..."
docker compose build
SSL_DIR="$STACK_DIR/ssl"
install -d -m 0750 "$SSL_DIR"
if [[ ! -f "$SSL_DIR/squid-ca-key.pem" || ! -f "$SSL_DIR/squid-ca-cert.pem" ]]; then
log "Generating TLS interception CA (one-time; CN=${CA_CN})..."
docker run --rm -v "$SSL_DIR:/out" --entrypoint openssl "$IMAGE" \
req -x509 -newkey rsa:4096 -sha256 -days "$CA_DAYS" -nodes \
-keyout /out/squid-ca-key.pem -out /out/squid-ca-cert.pem \
-subj "/CN=${CA_CN}/O=${CA_O}" \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign"
chmod 0600 "$SSL_DIR/squid-ca-key.pem"
chmod 0644 "$SSL_DIR/squid-ca-cert.pem"
log "CA created. Distribute $SSL_DIR/squid-ca-cert.pem to client trust stores."
else
log "CA already present in $SSL_DIR; leaving it."
fi
# ----------------------------------------------------------------------------
# Bring up the stack
# ----------------------------------------------------------------------------
log "Starting stack..."
docker compose up -d --remove-orphans
# ----------------------------------------------------------------------------
# Wait for health
# ----------------------------------------------------------------------------
log "Waiting for squid to become healthy (up to 120s)..."
deadline=$(( $(date +%s) + 120 ))
while (( $(date +%s) < deadline )); do
status=$(docker compose ps --format '{{.Service}} {{.Health}}' 2>/dev/null || true)
unhealthy=$(echo "$status" | awk '$2 != "healthy" && $2 != "" {print $1}')
if [[ -z "$unhealthy" && -n "$status" ]]; then
log "Squid healthy."
break
fi
sleep 5
done
echo
log "Stack status:"
docker compose ps
echo
CERT="$SSL_DIR/squid-ca-cert.pem"
cat <<EOF
================================================================
DEPLOYED -- Squid SSL-bump caching forward proxy
Proxy: http://<this-host>:${PROXY_PORT}
Stack dir: ${STACK_DIR}
CA cert: ${CERT}
1. Point clients at the proxy:
export http_proxy=http://<this-host>:${PROXY_PORT}
export https_proxy=http://<this-host>:${PROXY_PORT}
(apk: /etc/apk/repositories via http_proxy; apt: Acquire::http(s)::Proxy;
dnf: proxy= in /etc/dnf/dnf.conf)
2. Trust the CA on each client (so bumped HTTPS validates):
Debian/Ubuntu: cp ${CERT##*/} /usr/local/share/ca-certificates/squid-ca.crt && update-ca-certificates
Alpine: cp ${CERT##*/} /usr/local/share/ca-certificates/squid-ca.crt && update-ca-certificates
Alma/RHEL: cp ${CERT##*/} /etc/pki/ca-trust/source/anchors/squid-ca.pem && update-ca-trust
Browsers/Java keep their own trust stores -- import there too if needed.
3. Tune behavior, then re-render (lists are bind-mounted):
${STACK_DIR}/cache-domains.txt domains to cache hard (wildcards)
${STACK_DIR}/cache-domains.regex glob/regex cache patterns (optional)
${STACK_DIR}/splice-domains.txt domains to NOT decrypt
edit, then: cd ${STACK_DIR} && docker compose restart
Logs / cache stats:
docker compose logs -f
docker exec squid squid -k parse # validate config
docker exec squid tail -f /var/log/squid/access.log # TCP_HIT / TCP_MISS
Manage:
docker compose pull >/dev/null; docker compose build && docker compose up -d # update
docker compose down # stop, keep cache + CA
docker compose down -v # stop, WIPE cache + leaf-cert DB (CA on host survives)
SECURITY: keep ${SSL_DIR}/squid-ca-key.pem secret (it can MITM every client
that trusts it). It is 0600 root and never leaves this host. Only intercept
traffic on networks/devices you own.
Or just re-run this script -- it's idempotent.
================================================================
EOF
# IMPORTANT: do not put any code below this exit. Everything after the
# __ARCHIVE_BELOW__ marker is the embedded tar.gz payload (base64), appended by
# build.sh.
exit 0
__ARCHIVE_BELOW__
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
+51
View File
@@ -0,0 +1,51 @@
# Squid SSL-bump caching forward proxy.
#
# Built locally from ./Dockerfile (the repo's one self-built image) -- there is
# no upstream ssl-bump image we want to trust in a hardened setup. The TLS
# interception CA lives on the host in ./ssl (generated by deploy.sh, mounted
# read-only); the leaf-cert DB, cache, and logs are named volumes.
#
# A published Docker port BYPASSES the host INPUT firewall, so BIND_ADDR should
# pin the listener to a trusted interface and TRUSTED_CIDR (Squid http_access)
# is the real access gate.
name: squid
volumes:
squid-cache:
squid-ssl-db:
squid-logs:
services:
squid:
build:
context: .
image: ${SQUID_IMAGE_TAG:-automations/squid:latest}
container_name: squid
restart: unless-stopped
ports:
- "${BIND_ADDR:-0.0.0.0}:${PROXY_PORT:-3128}:3128"
environment:
TRUSTED_CIDR: "${TRUSTED_CIDR}"
CACHE_SIZE_MB: "${CACHE_SIZE_MB:-5000}"
MAX_OBJECT_SIZE_MB: "${MAX_OBJECT_SIZE_MB:-256}"
CACHE_MEM_MB: "${CACHE_MEM_MB:-256}"
DYNAMIC_CERT_MEM_MB: "${DYNAMIC_CERT_MEM_MB:-8}"
CACHE_ONLY_LISTED: "${CACHE_ONLY_LISTED:-0}"
VISIBLE_HOSTNAME: "${VISIBLE_HOSTNAME:-squid-proxy}"
volumes:
- ./ssl:/etc/squid/ssl:ro
- ./splice-domains.txt:/etc/squid/splice-domains.txt:ro
- ./cache-domains.txt:/etc/squid/cache-domains.txt:ro
- ./cache-domains.regex:/etc/squid/cache-domains.regex:ro
- squid-cache:/var/cache/squid
- squid-ssl-db:/var/lib/squid/ssl_db
- squid-logs:/var/log/squid
healthcheck:
# `squid -k check` signals the running master process via its pid file --
# reliable and always present (no dependency on squidclient/nc).
test: ["CMD-SHELL", "squid -k check -f /etc/squid/squid.conf || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
+145
View File
@@ -0,0 +1,145 @@
#!/bin/sh
#
# entrypoint.sh -- container start-up for the Squid SSL-bump caching proxy.
#
# 1. Render /etc/squid/squid.conf from squid.conf.tmpl (@VAR@ -> env).
# 2. Generate the cache-policy include from cache-domains.txt / .regex and the
# CACHE_ONLY_LISTED toggle (boost ACLs, storage gate, per-domain TTLs).
# 3. Stage the CA into a squid-readable tmpfs copy (host key stays 0600 root).
# 4. Init the dynamic-cert DB + cache_dir structure (idempotent).
# 5. Validate the config, then exec squid in the foreground.
#
# Runs as root (needs chown/install); squid then drops to cache_effective_user.
set -eu
TMPL=/etc/squid/squid.conf.tmpl
CONF=/etc/squid/squid.conf
GEN_DIR=/etc/squid/conf.d
GEN="$GEN_DIR/cache-policy.conf"
SSL_SRC=/etc/squid/ssl
SSL_RUN=/run/squid
SSL_DB=/var/lib/squid/ssl_db
CACHE_DOMAINS=/etc/squid/cache-domains.txt
CACHE_REGEX=/etc/squid/cache-domains.regex
# Tunables -- docker-compose passes these from .env; defaults keep the image
# runnable on its own for a smoke test.
: "${TRUSTED_CIDR:=127.0.0.1/32}"
: "${CACHE_SIZE_MB:=5000}"
: "${MAX_OBJECT_SIZE_MB:=256}"
: "${CACHE_MEM_MB:=256}"
: "${DYNAMIC_CERT_MEM_MB:=8}"
: "${CACHE_ONLY_LISTED:=0}"
: "${VISIBLE_HOSTNAME:=squid-proxy}"
log() { printf '[entrypoint] %s\n' "$*" >&2; }
# A list file "has entries" if it holds >=1 non-blank, non-comment line.
has_entries() { [ -f "$1" ] && grep -qE '^[[:space:]]*[^#[:space:]]' "$1"; }
# ---------------------------------------------------------------------------
# 1. Render the static config from the template (@VAR@ placeholders).
# ---------------------------------------------------------------------------
sed \
-e "s|@TRUSTED_CIDR@|${TRUSTED_CIDR}|g" \
-e "s|@CACHE_SIZE_MB@|${CACHE_SIZE_MB}|g" \
-e "s|@MAX_OBJECT_SIZE_MB@|${MAX_OBJECT_SIZE_MB}|g" \
-e "s|@CACHE_MEM_MB@|${CACHE_MEM_MB}|g" \
-e "s|@DYNAMIC_CERT_MEM_MB@|${DYNAMIC_CERT_MEM_MB}|g" \
-e "s|@VISIBLE_HOSTNAME@|${VISIBLE_HOSTNAME}|g" \
"$TMPL" > "$CONF"
# ---------------------------------------------------------------------------
# 2. Generate the cache-policy include from the domain lists + toggle.
# Storage gate runs first (whether to store); per-domain refresh_patterns
# only tune freshness of what survived the gate.
# ---------------------------------------------------------------------------
mkdir -p "$GEN_DIR"
{
echo "# AUTO-GENERATED by entrypoint.sh at container start -- do not edit."
echo "# Source: cache-domains.txt / cache-domains.regex + CACHE_ONLY_LISTED."
echo
# Boost ACLs are only emitted when their backing file is non-empty, else
# squid would error on an empty ACL.
exc=""
if has_entries "$CACHE_DOMAINS"; then
echo 'acl boost dstdomain "/etc/squid/cache-domains.txt"'
exc="$exc !boost"
fi
if has_entries "$CACHE_REGEX"; then
echo 'acl boost_re dstdom_regex "/etc/squid/cache-domains.regex"'
exc="$exc !boost_re"
fi
cat <<'ACLS'
acl has_query urlpath_regex \?
acl dyn_ext urlpath_regex (/cgi-bin/|\.(cgi|php|aspx?|jsp)$)
acl html_ext urlpath_regex \.html?(\?|$)
acl html_ct rep_mime_type -i ^text/html
# Storage gate: never store HTML (by ext or content-type) or dynamic content.
cache deny html_ext
cache deny dyn_ext
ACLS
# Query strings are dynamic EXCEPT on boosted domains (versioned static
# assets like app.js?v=123 should still cache there).
echo "cache deny has_query${exc}"
echo 'store_miss deny html_ct'
if [ "$CACHE_ONLY_LISTED" = "1" ]; then
echo
echo "# CACHE_ONLY_LISTED=1: store ONLY the listed domains."
if has_entries "$CACHE_DOMAINS"; then echo 'cache allow boost'; fi
if has_entries "$CACHE_REGEX"; then echo 'cache allow boost_re'; fi
echo 'cache deny all'
fi
if has_entries "$CACHE_DOMAINS"; then
echo
echo "# Boost: long TTL + force-cache for each listed wildcard domain."
grep -E '^[[:space:]]*[^#[:space:]]' "$CACHE_DOMAINS" | while read -r dom _rest; do
# ".example.com" / "example.com" -> "example[.]com" ([.] matches a
# literal dot portably -- avoids sed backslash-escaping quirks).
d=$(printf '%s' "$dom" | sed 's/^\.//; s/\./[.]/g')
printf 'refresh_pattern -i %s 1440 100%% 43200 override-expire ignore-private ignore-no-store override-lastmod\n' \
"^https?://([^/]+[.])?${d}/"
done
fi
} > "$GEN"
# ---------------------------------------------------------------------------
# 3. Stage the CA into a squid-readable tmpfs copy. The host key is 0600 root
# (mounted read-only); root copies it to /run owned by squid so the signer
# can read it without loosening the host file.
# ---------------------------------------------------------------------------
if [ ! -f "$SSL_SRC/squid-ca-cert.pem" ] || [ ! -f "$SSL_SRC/squid-ca-key.pem" ]; then
log "FATAL: CA missing in $SSL_SRC (need squid-ca-cert.pem + squid-ca-key.pem)."
exit 1
fi
install -d -m 0710 -o squid -g squid "$SSL_RUN"
install -m 0444 -o squid -g squid "$SSL_SRC/squid-ca-cert.pem" "$SSL_RUN/ca-cert.pem"
install -m 0400 -o squid -g squid "$SSL_SRC/squid-ca-key.pem" "$SSL_RUN/ca-key.pem"
# ---------------------------------------------------------------------------
# 4. Init the dynamic-cert DB + cache_dir structure (idempotent).
# ---------------------------------------------------------------------------
if [ ! -s "$SSL_DB/index.txt" ]; then
log "Initializing TLS cert DB at $SSL_DB..."
find "$SSL_DB" -mindepth 1 -delete 2>/dev/null || true
/usr/lib/squid/security_file_certgen -c -s "$SSL_DB" -M "${DYNAMIC_CERT_MEM_MB}MB"
fi
chown -R squid:squid "$SSL_DB" /var/cache/squid /var/log/squid 2>/dev/null || true
if [ ! -d /var/cache/squid/00 ]; then
log "Initializing cache_dir structure..."
squid -f "$CONF" -z --foreground || squid -f "$CONF" -z || true
fi
# ---------------------------------------------------------------------------
# 5. Validate the rendered + generated config, then hand off to squid.
# ---------------------------------------------------------------------------
squid -k parse -f "$CONF"
log "Starting squid (visible_hostname=${VISIBLE_HOSTNAME})..."
exec squid -N -f "$CONF" "$@"
+30
View File
@@ -0,0 +1,30 @@
# splice-domains.txt
#
# Domains Squid must NOT decrypt. With SSL-bump, "splice" = transparent
# passthrough: the client's TLS goes straight to the origin, so these are
# NOT cached and NOT inspected. Use for cert-pinned apps, banking, app-store /
# OS update channels, and anything that breaks under interception.
#
# One entry per line; a LEADING DOT matches all subdomains (.apple.com matches
# www.apple.com, gs.apple.com, ...). Full-line "#" comments only.
#
# IMPORTANT: do not also list a domain here AND in cache-domains.txt -- splice
# wins, so it would never cache. Tune this list for your environment.
# ── OS / app-store update channels (commonly cert-pinned) ──
.apple.com
.icloud.com
.mzstatic.com
.windowsupdate.com
.update.microsoft.com
.android.clients.google.com
.play.googleapis.com
# ── Banking / payments (examples -- add your own) ──
.paypal.com
.stripe.com
# ── Messaging / pinned services ──
.whatsapp.net
.signal.org
.telegram.org
+76
View File
@@ -0,0 +1,76 @@
# squid.conf.tmpl
#
# Rendered to /etc/squid/squid.conf by entrypoint.sh at container start:
# - @VAR@ placeholders are substituted from the environment (.env);
# - the cache policy (boost ACLs, storage gate, per-domain refresh_patterns)
# is generated from cache-domains.txt / .regex into the include below.
#
# Role: EXPLICIT forward proxy with SSL-bump. Clients set HTTP(S)_PROXY to this
# host:3128. (Transparent/intercepting mode is future work -- see README.)
visible_hostname @VISIBLE_HOSTNAME@
# ── Listener: explicit proxy, SSL-bump enabled ──────────────────────────────
# tls-cert/tls-key are the local CA used to mint per-host leaf certs on the fly.
# The key is staged into /run by the entrypoint so the squid user can read it
# while the on-host key file stays 0600 root.
http_port 3128 ssl-bump \
tls-cert=/run/squid/ca-cert.pem \
tls-key=/run/squid/ca-key.pem \
generate-host-certificates=on \
dynamic_cert_mem_cache_size=@DYNAMIC_CERT_MEM_MB@MB
sslcrtd_program /usr/lib/squid/security_file_certgen -s /var/lib/squid/ssl_db -M @DYNAMIC_CERT_MEM_MB@MB
sslcrtd_children 8 startup=1 idle=1
# Validate UPSTREAM certificates -- never silently MITM a broken origin cert.
sslproxy_cert_error deny all
# ── SSL-bump policy: peek at SNI → splice allowlist → bump everything else ──
acl step1 at_step SslBump1
acl splice_dom ssl::server_name "/etc/squid/splice-domains.txt"
ssl_bump peek step1
ssl_bump splice splice_dom
ssl_bump bump all
# ── Access control: deny-by-default, trusted clients only ───────────────────
acl SSL_ports port 443
acl Safe_ports port 80 443 21 70 210 1025-65535 280 488 591 777
acl CONNECT method CONNECT
http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports
# Cache manager: localhost only (used by the container healthcheck).
http_access allow localhost manager
http_access deny manager
acl trusted_clients src @TRUSTED_CIDR@
http_access allow localhost
http_access allow trusted_clients
http_access deny all
# ── Cache sizing ────────────────────────────────────────────────────────────
cache_mem @CACHE_MEM_MB@ MB
maximum_object_size_in_memory 1 MB
maximum_object_size @MAX_OBJECT_SIZE_MB@ MB
cache_dir ufs /var/cache/squid @CACHE_SIZE_MB@ 16 256
coredump_dir /var/cache/squid
# ── Cache policy (generated: boost ACLs, storage gate, per-domain TTLs) ─────
include /etc/squid/conf.d/cache-policy.conf
# Base refresh patterns (fallbacks; per-domain boosts live in the include and
# are matched first). Packages get long, confident TTLs; everything else is
# conservative and revalidates.
refresh_pattern -i \.(deb|rpm|apk|pkg|tar\.(gz|xz|zst|bz2)|whl|jar)$ 10080 100% 43200
refresh_pattern . 0 20% 4320
# ── Logging ─────────────────────────────────────────────────────────────────
access_log stdio:/var/log/squid/access.log
cache_log /var/log/squid/cache.log
logfile_rotate 7
# ── Privilege drop / lifecycle ──────────────────────────────────────────────
cache_effective_user squid
pid_filename /run/squid.pid
shutdown_lifetime 5 seconds