Compare commits
8 Commits
feature/so
...
a58f5a3406
| Author | SHA1 | Date | |
|---|---|---|---|
| a58f5a3406 | |||
| 1f572927a1 | |||
| c9039feeab | |||
| 7144699a77 | |||
| a3cb4bad5b | |||
| 4e945a1426 | |||
| 44a8560f5a | |||
| 0826bb4502 |
30
README.md
30
README.md
@@ -48,13 +48,13 @@ label:
|
|||||||
- "traefik.http.routers.service-name.entrypoints=websecure"
|
- "traefik.http.routers.service-name.entrypoints=websecure"
|
||||||
```
|
```
|
||||||
|
|
||||||
Besides the entrypoint setup, I add cloudflare proxy (for exposing real ip to access.log for crowdsec to read), crowdsec-firewall-bouncer, compression with brotli middlrewares method in traefik.yml and dynamic.yml
|
Besides the entrypoint setup, I add CrowdSec firewall bouncer plus a compression middleware (brotli/gzip/zstd) defined in `dynamic.yml`. Cloudflare’s IP ranges are injected directly into `traefik.yml` by a helper script, so no extra plugin middleware is required anymore.
|
||||||
|
|
||||||
Adding middlewares is also guided by labels:
|
Adding middlewares is also guided by labels:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
label:
|
label:
|
||||||
- "traefik.http.routers.service-name.middlewares=cloudflarewarp@file,crowdsec@file,compress-middleware@file"
|
- "traefik.http.routers.service-name.middlewares=crowdsec@file,compress-middleware@file"
|
||||||
```
|
```
|
||||||
|
|
||||||
The order of middlewares is meaningful.
|
The order of middlewares is meaningful.
|
||||||
@@ -73,11 +73,33 @@ labels:
|
|||||||
- "traefik.http.routers.ghost.rule=Host(`blog.gbanyan.net`)"
|
- "traefik.http.routers.ghost.rule=Host(`blog.gbanyan.net`)"
|
||||||
- "traefik.http.services.ghost.loadbalancer.server.port=2368"
|
- "traefik.http.services.ghost.loadbalancer.server.port=2368"
|
||||||
- "traefik.http.routers.ghost.tls.certresolver=letsencrypt"
|
- "traefik.http.routers.ghost.tls.certresolver=letsencrypt"
|
||||||
- "traefik.http.routers.ghost.middlewares=cloudflarewarp@file,crowdsec@file,compress-middleware@file"
|
- "traefik.http.routers.ghost.middlewares=crowdsec@file,compress-middleware@file"
|
||||||
- "com.centurylinklabs.watchtower.enable=true"
|
- "com.centurylinklabs.watchtower.enable=true"
|
||||||
- "traefik.docker.network=traefik_default"
|
- "traefik.docker.network=traefik_default"
|
||||||
```
|
```
|
||||||
|
|
||||||
I mount the access.log for crowdsec firewall to read.
|
I mount the access.log for crowdsec firewall to read.
|
||||||
|
|
||||||
PS: Because I access my traefik dashboard through my local network. I commented out the authetication method for dashboard.
|
PS: Because I access my traefik dashboard through my local network. I commented out the authetication method for dashboard.
|
||||||
|
|
||||||
|
## Discussion and Changelog
|
||||||
|
|
||||||
|
1. Traefik vs Nginx
|
||||||
|
- Performance: Nginx is still better at high traffic. After all it is written in C. Traefik 3 though claims it has higher 20% performance than before. The latency still showed a little higher than nginx.
|
||||||
|
- Docker Deployment Ease: Traefik is easier for docker service deployment. In my environment, I can assign each docker stack with labels and then guides the traefik to add Let's encrypt SSL.
|
||||||
|
|
||||||
|
2. ChangeLog:
|
||||||
|
|
||||||
|
- 2025.4.21 Add the defaulthost rule for container name for lazy writing. But commented out for precision.
|
||||||
|
- 2025.4.21 Fix the trusted IP settings; later replaced by an internal updater instead of the traefik-plugin-cloudflare.
|
||||||
|
- 2025.4.18 Add Souin HTTP Cache Middleware (in feature branch, not merge into main)
|
||||||
|
- 2025.4.18 Temp disable the compression middleware. It has MIME type bugs.
|
||||||
|
|
||||||
|
## Notes on Host Networking
|
||||||
|
|
||||||
|
Traefik currently runs with `network_mode: host` so it can bind directly to both `10.0.0.225` (public) and `192.168.50.4` (internal) entrypoints. Moving back to bridge mode would break that dual-IP isolation because Docker cannot publish the same container port on two different host interfaces. Host networking also means:
|
||||||
|
|
||||||
|
- Traefik reaches app containers like any other host process, ignoring `traefik.docker.network` labels.
|
||||||
|
- Linux handles firewalling/routing between the two interfaces; Docker’s conntrack optimizations aren’t used.
|
||||||
|
|
||||||
|
If you ever want to switch to bridge networking, you’d need either separate Traefik instances (one per subnet) or an external L4 proxy in front of a single Traefik that listens on generic `:80/:443` ports. For now the host-mode trade-off is intentional to keep the internal/external split simple.
|
||||||
|
|||||||
@@ -39,3 +39,12 @@ services:
|
|||||||
# external: true
|
# external: true
|
||||||
# internal_traefik_default:
|
# internal_traefik_default:
|
||||||
# external: true
|
# external: true
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: traefik_default
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.19.0.0/16
|
||||||
|
gateway: 172.19.0.1
|
||||||
|
|
||||||
|
|||||||
29
dynamic.yml
29
dynamic.yml
@@ -1,15 +1,5 @@
|
|||||||
http:
|
http:
|
||||||
middlewares:
|
middlewares:
|
||||||
block-ip-access:
|
|
||||||
headers:
|
|
||||||
customRequestHeaders:
|
|
||||||
Host: "" # This will catch requests with no Host header or invalid ones
|
|
||||||
cloudflarewarp:
|
|
||||||
plugin:
|
|
||||||
cloudflare:
|
|
||||||
trustedCIDRs: []
|
|
||||||
overwriteRequestHeader: true
|
|
||||||
debug: true
|
|
||||||
crowdsec:
|
crowdsec:
|
||||||
plugin:
|
plugin:
|
||||||
bouncer:
|
bouncer:
|
||||||
@@ -31,24 +21,15 @@ http:
|
|||||||
- application/json
|
- application/json
|
||||||
- text/plain
|
- text/plain
|
||||||
routers:
|
routers:
|
||||||
block-direct-access:
|
qbit:
|
||||||
rule: "HostRegexp(`{host:.+}`)" # Matches any host
|
rule: Host(`qbit.gbanyan.net`)
|
||||||
service: noop@internal
|
service: qbit
|
||||||
priority: 1 # Low priority to catch unmatched requests
|
|
||||||
entryPoints:
|
|
||||||
- web
|
|
||||||
- websecure
|
|
||||||
middlewares:
|
|
||||||
- block-ip-access
|
|
||||||
netdata:
|
|
||||||
rule: Host(`netdata.gbanyan.net`)
|
|
||||||
service: netdata
|
|
||||||
entryPoints: ["internal_websecure"]
|
entryPoints: ["internal_websecure"]
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
services:
|
services:
|
||||||
netdata:
|
qbit:
|
||||||
loadBalancer:
|
loadBalancer:
|
||||||
servers:
|
servers:
|
||||||
- url: "http://127.0.0.1:19999"
|
- url: "http://192.168.50.4:8083"
|
||||||
|
|||||||
107
scripts/update_cloudflare_ips.py
Executable file
107
scripts/update_cloudflare_ips.py
Executable file
@@ -0,0 +1,107 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Refresh Cloudflare IP ranges in Traefik config and restart Traefik if needed."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Iterable, List
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
|
CLOUDFLARE_IPV4_URL = "https://www.cloudflare.com/ips-v4"
|
||||||
|
CLOUDFLARE_IPV6_URL = "https://www.cloudflare.com/ips-v6"
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
TRAEFIK_CONFIG_PATH = REPO_ROOT / "traefik.yml"
|
||||||
|
ENTRYPOINTS = [" web:", " websecure:"]
|
||||||
|
USER_AGENT = "TraefikCloudflareUpdater/1.0 (+https://gbanyan.net)"
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_ip_ranges() -> List[str]:
|
||||||
|
ips: List[str] = []
|
||||||
|
for url in (CLOUDFLARE_IPV4_URL, CLOUDFLARE_IPV6_URL):
|
||||||
|
req = Request(url, headers={"User-Agent": USER_AGENT})
|
||||||
|
with urlopen(req, timeout=10) as resp: # noqa: S310 (trusted upstream)
|
||||||
|
for raw_line in resp.read().decode().splitlines():
|
||||||
|
line = raw_line.strip()
|
||||||
|
if line and not line.startswith("#"):
|
||||||
|
ips.append(line)
|
||||||
|
# Remove duplicates while preserving order
|
||||||
|
seen = set()
|
||||||
|
deduped: List[str] = []
|
||||||
|
for ip in ips:
|
||||||
|
if ip not in seen:
|
||||||
|
deduped.append(ip)
|
||||||
|
seen.add(ip)
|
||||||
|
return deduped
|
||||||
|
|
||||||
|
|
||||||
|
def update_trusted_ips(original: str, ips: Iterable[str], entrypoint_label: str) -> str:
|
||||||
|
try:
|
||||||
|
entry_idx = original.index(entrypoint_label)
|
||||||
|
except ValueError:
|
||||||
|
raise RuntimeError(f"EntryPoint {entrypoint_label.strip()} not found in traefik.yml") from None
|
||||||
|
|
||||||
|
forwarded_token = "forwardedHeaders:\n"
|
||||||
|
fwd_idx = original.index(forwarded_token, entry_idx)
|
||||||
|
line_start = original.rfind("\n", 0, fwd_idx) + 1
|
||||||
|
indent = original[line_start:fwd_idx]
|
||||||
|
inner_indent = indent + " "
|
||||||
|
list_indent = inner_indent + " "
|
||||||
|
|
||||||
|
block_start = fwd_idx + len(forwarded_token)
|
||||||
|
cursor = block_start
|
||||||
|
while cursor < len(original):
|
||||||
|
next_newline = original.find("\n", cursor)
|
||||||
|
if next_newline == -1:
|
||||||
|
next_newline = len(original)
|
||||||
|
line = original[cursor:next_newline]
|
||||||
|
stripped = line.lstrip()
|
||||||
|
current_indent = len(line) - len(stripped)
|
||||||
|
if not stripped.startswith("-") and current_indent <= len(indent):
|
||||||
|
break
|
||||||
|
cursor = next_newline + 1
|
||||||
|
block_end = cursor
|
||||||
|
|
||||||
|
new_block = forwarded_token
|
||||||
|
new_block += f"{inner_indent}trustedIPs:\n"
|
||||||
|
for ip in ips:
|
||||||
|
new_block += f"{list_indent}- \"{ip}\"\n"
|
||||||
|
|
||||||
|
return original[:fwd_idx] + new_block + original[block_end:]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ips = fetch_ip_ranges()
|
||||||
|
config_text = TRAEFIK_CONFIG_PATH.read_text()
|
||||||
|
before_hash = hashlib.sha256(config_text.encode()).hexdigest()
|
||||||
|
|
||||||
|
for entry_label in ENTRYPOINTS:
|
||||||
|
config_text = update_trusted_ips(config_text, ips, entry_label)
|
||||||
|
|
||||||
|
after_hash = hashlib.sha256(config_text.encode()).hexdigest()
|
||||||
|
if after_hash == before_hash:
|
||||||
|
print("Traefik trusted IP ranges already up-to-date.")
|
||||||
|
return
|
||||||
|
|
||||||
|
TRAEFIK_CONFIG_PATH.write_text(config_text)
|
||||||
|
print(f"Updated {TRAEFIK_CONFIG_PATH} with {len(ips)} Cloudflare IP ranges.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["docker", "compose", "up", "-d", "traefik"],
|
||||||
|
check=True,
|
||||||
|
cwd=REPO_ROOT,
|
||||||
|
)
|
||||||
|
print("Traefik container restarted via docker compose.")
|
||||||
|
except subprocess.CalledProcessError as exc: # pragma: no cover
|
||||||
|
print("Failed to restart Traefik via docker compose:", exc, file=sys.stderr)
|
||||||
|
sys.exit(exc.returncode)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
print(f"Error: {exc}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
105
traefik.yml
105
traefik.yml
@@ -1,14 +1,27 @@
|
|||||||
## STATIC CONFIGURATION
|
## STATIC CONFIGURATION
|
||||||
|
|
||||||
log:
|
log:
|
||||||
level: "DEBUG"
|
level: "INFO"
|
||||||
filePath: "/var/log/traefik/traefik.log"
|
filePath: "/var/log/traefik/traefik.log"
|
||||||
accessLog:
|
accessLog:
|
||||||
filePath: "/var/log/traefik/access.log"
|
filePath: "/var/log/traefik/access.log"
|
||||||
|
bufferingSize: 256
|
||||||
filters:
|
filters:
|
||||||
statusCodes:
|
statusCodes:
|
||||||
- "200-299" # log successful http requests
|
- "400-599" # focus on failed http requests
|
||||||
- "400-599" # log failed http requests
|
fields:
|
||||||
|
defaultMode: drop
|
||||||
|
names:
|
||||||
|
ClientHost: keep
|
||||||
|
ClientPort: keep
|
||||||
|
Duration: keep
|
||||||
|
RequestMethod: keep
|
||||||
|
RequestPath: keep
|
||||||
|
RequestProtocol: keep
|
||||||
|
RouterName: keep
|
||||||
|
ServiceName: keep
|
||||||
|
ServiceURL: keep
|
||||||
|
Status: keep
|
||||||
|
|
||||||
api:
|
api:
|
||||||
insecure: false
|
insecure: false
|
||||||
@@ -16,34 +29,32 @@ api:
|
|||||||
|
|
||||||
entryPoints:
|
entryPoints:
|
||||||
web:
|
web:
|
||||||
address: "10.0.0.225:80"
|
address: "10.0.0.225:80"
|
||||||
forwardedHeaders:
|
forwardedHeaders:
|
||||||
trustedIPs: &trustedIps
|
trustedIPs:
|
||||||
# Start of Cloudlare's public IP list
|
- "173.245.48.0/20"
|
||||||
- 103.21.244.0/22
|
- "103.21.244.0/22"
|
||||||
- 103.22.200.0/22
|
- "103.22.200.0/22"
|
||||||
- 103.31.4.0/22
|
- "103.31.4.0/22"
|
||||||
- 104.16.0.0/13
|
- "141.101.64.0/18"
|
||||||
- 104.24.0.0/14
|
- "108.162.192.0/18"
|
||||||
- 108.162.192.0/18
|
- "190.93.240.0/20"
|
||||||
- 131.0.72.0/22
|
- "188.114.96.0/20"
|
||||||
- 141.101.64.0/18
|
- "197.234.240.0/22"
|
||||||
- 162.158.0.0/15
|
- "198.41.128.0/17"
|
||||||
- 172.64.0.0/13
|
- "162.158.0.0/15"
|
||||||
- 173.245.48.0/20
|
- "104.16.0.0/13"
|
||||||
- 188.114.96.0/20
|
- "104.24.0.0/14"
|
||||||
- 190.93.240.0/20
|
- "172.64.0.0/13"
|
||||||
- 197.234.240.0/22
|
- "131.0.72.0/22"
|
||||||
- 198.41.128.0/17
|
- "2400:cb00::/32"
|
||||||
- 2400:cb00::/32
|
- "2606:4700::/32"
|
||||||
- 2606:4700::/32
|
- "2803:f800::/32"
|
||||||
- 2803:f800::/32
|
- "2405:b500::/32"
|
||||||
- 2405:b500::/32
|
- "2405:8100::/32"
|
||||||
- 2405:8100::/32
|
- "2a06:98c0::/29"
|
||||||
- 2a06:98c0::/29
|
- "2c0f:f248::/32"
|
||||||
- 2c0f:f248::/32
|
http:
|
||||||
# End of Cloudlare's public IP list
|
|
||||||
http:
|
|
||||||
redirections: # HTTPS redirection (80 to 443)
|
redirections: # HTTPS redirection (80 to 443)
|
||||||
entryPoint:
|
entryPoint:
|
||||||
to: "websecure" # The target element
|
to: "websecure" # The target element
|
||||||
@@ -51,8 +62,29 @@ entryPoints:
|
|||||||
websecure:
|
websecure:
|
||||||
address: "10.0.0.225:443"
|
address: "10.0.0.225:443"
|
||||||
forwardedHeaders:
|
forwardedHeaders:
|
||||||
# Reuse the list of Cloudflare's public IPs from above
|
trustedIPs:
|
||||||
trustedIPs: *trustedIps
|
- "173.245.48.0/20"
|
||||||
|
- "103.21.244.0/22"
|
||||||
|
- "103.22.200.0/22"
|
||||||
|
- "103.31.4.0/22"
|
||||||
|
- "141.101.64.0/18"
|
||||||
|
- "108.162.192.0/18"
|
||||||
|
- "190.93.240.0/20"
|
||||||
|
- "188.114.96.0/20"
|
||||||
|
- "197.234.240.0/22"
|
||||||
|
- "198.41.128.0/17"
|
||||||
|
- "162.158.0.0/15"
|
||||||
|
- "104.16.0.0/13"
|
||||||
|
- "104.24.0.0/14"
|
||||||
|
- "172.64.0.0/13"
|
||||||
|
- "131.0.72.0/22"
|
||||||
|
- "2400:cb00::/32"
|
||||||
|
- "2606:4700::/32"
|
||||||
|
- "2803:f800::/32"
|
||||||
|
- "2405:b500::/32"
|
||||||
|
- "2405:8100::/32"
|
||||||
|
- "2a06:98c0::/29"
|
||||||
|
- "2c0f:f248::/32"
|
||||||
http3: {}
|
http3: {}
|
||||||
internal_web:
|
internal_web:
|
||||||
address: "192.168.50.4:80"
|
address: "192.168.50.4:80"
|
||||||
@@ -65,7 +97,7 @@ entryPoints:
|
|||||||
address: "192.168.50.4:443"
|
address: "192.168.50.4:443"
|
||||||
http3: {}
|
http3: {}
|
||||||
metrics:
|
metrics:
|
||||||
address: "127.0.0.1:8082"
|
address: ":8082"
|
||||||
dashboard:
|
dashboard:
|
||||||
address: "127.0.0.1:9090"
|
address: "127.0.0.1:9090"
|
||||||
|
|
||||||
@@ -76,7 +108,7 @@ global:
|
|||||||
providers:
|
providers:
|
||||||
docker:
|
docker:
|
||||||
exposedByDefault: false
|
exposedByDefault: false
|
||||||
# network: traefik_default # Ensure this matches the Docker network
|
# defaultRule: "Host(`{{ .ContainerName }}.gbanyan.net`)"
|
||||||
file:
|
file:
|
||||||
filename: "/dynamic.yml" # Enable dynamic configuration file
|
filename: "/dynamic.yml" # Enable dynamic configuration file
|
||||||
certificatesResolvers:
|
certificatesResolvers:
|
||||||
@@ -97,9 +129,6 @@ metrics:
|
|||||||
|
|
||||||
experimental:
|
experimental:
|
||||||
plugins:
|
plugins:
|
||||||
cloudflare:
|
|
||||||
moduleName: github.com/agence-gaya/traefik-plugin-cloudflare
|
|
||||||
version: v1.2.0
|
|
||||||
bouncer:
|
bouncer:
|
||||||
moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||||
version: v1.4.2
|
version: v1.4.2
|
||||||
|
|||||||
Reference in New Issue
Block a user