9.6 KiB
+++ categories = ["software"] tags = ["proxy","infrastructure","traefik","crowdsec"] date = 2025-08-06T08:00:00-05:00 description = "Centralizing external service access on an edge VM with Traefik reverse proxying and CrowdSec intrusion prevention." draft = false slug = "edge-reverse-proxy" title = "🧭 Centralized, Secure Reverse Proxy at the edge" author = "nicholas" +++
In a [previous post ]({{< relref "posts/infrastructure-overhaul" >}}) I recorded the state of my network at the time, which included no centralized reverse proxy. There was no need. I accessed my services locally and each of the service host ran its own reverse proxy on its own IP. I show this configuration in the diagram below.
✅ Multiple Reverse Proxies (LOCAL ONLY)
graph TD
subgraph lan[🏠LAN]
client[🧑💻Client]
subgraph hostA["🖥️Docker Host"]
reverseproxyA["🚪Reverse Proxy"]
serviceAA["🧠LLM"]
end
subgraph hostB["🖥️Kubernetes Host"]
reverseproxyB["🚪Reverse Proxy"]
serviceBA["🖼️Photos"]
serviceBB["📝Blog"]
end
end
client -->|192.168.1.10:443|reverseproxyA
client -->|192.168.1.11:443|reverseproxyB
reverseproxyA -->|llm.domain.com|serviceAA
reverseproxyB -->|photos.domain.com|serviceBA
reverseproxyB -->|blog.domain.com|serviceBB
However, now I would like to access certain services from beyond my local network, and not just for myself. I want them to be accessible to all web users, otherwise I would simply set up a client-to-site VPN and be done. I want them to be accessible over HTTPS.
If I want to expose a service on two different local hosts on a single port (:443, HTTPS) then I need a single host to bind to the single port. I show what this would look like in the diagram below.
⚠️ A single public IP and port 443 can only be forwarded to one internal IP at a time.
✅ Single Edge Reverse Proxy
graph TD
subgraph wan[🌐WAN]
client[🧑💻Client]
publicip["172.153.14.23:443"]
end
subgraph router[🛡️Router]
forward["WAN:443 → LAN:443"]
end
subgraph lan[🏠LAN]
subgraph hostEdge["🖥️Edge Host"]
reverseproxy["🚪Edge Reverse Proxy"]
end
hostA["🖥️ Docker Host"]
hostB["🖥️ Kubernetes Host"]
end
client -->|HTTPS to domain| publicip
publicip -->|HTTPS traffic| router
router -->|NAT| reverseproxy
reverseproxy -->|192.168.1.10:443|hostA
reverseproxy --> |192.168.1.11:443|hostB
➡️ Effects
New host / VM
I will need a new host to run my reverse proxy.
Unencrypted traffic
Moving from having multiple reverse proxies (each terminating TLS) to a single edge reverse proxy means TLS termination will happen at the edge and all traffic inside the LAN is now unencrypted. This is fine.
DNS Configuration
DNS entries will need to be updated to point to new proxy host.
Automatic TLS cert renewal
Kubernetes ingress controllers will no longer manage TLS certificates. This responsibility will shift to Traefik to simplify things.
IPS
Since I am allowing traffic through port 443 into my LAN, I will want a way to prevent rampant bots from molesting my network. I will use an IPS called CrowdSec.
Firewall
Sometime later, I may configure a router / firewall in front of the reverse proxy to preemptively block IPs from outside of the USA. Much of the suspected malicious traffic originates from strange foreign countries like Bulgaria, China, etc.
🖥️ New VM / Edge Host
The VM which will run my edge services (reverse proxy, IPS) will need very few resources. I set it up with the following specs:
| Resource | Specification |
|---|---|
| OS | Ubuntu Server 24 (minimal) |
| vCPU | 1 |
| RAM | 2048 MB |
| Storage | 10 GB |
All I need to do is:
- Install Docker
- Install, configure
ufw
🚪Reverse Proxy + IPS
I run these two programs in Docker containers. The diagram below shows the directory structure of this stack. I do not bother adding configuration files to this post. Security through obscurity
Directory structure
📦ege-stack-config
├─📂crowdsec
│ └─📜acquis.yaml
├─📂traefik
│ ├─📂dynamic
│ │ ├─📜middlewares.yaml
│ │ ├─📜routers.yaml
│ │ └─📜services.yaml
│ └─📜traefik.yaml
├─📂logs
│ └─📜access.log
└─🐋docker-compose.yaml
Traefik performs its normal functions as a reverse proxy, generating access logs for incoming HTTPS traffic. CrowdSec reads these logs, parses them, and generates a decision as to whether an IP should be banned. CrowdSec sends this along to a bouncer to enforce the decision. For my bouncer, I am using a Traefik plugin aptly named Crowdsec Bouncer Traefik plugin.
🧪 Test it
After some time, I go check whether CrowdSec has banned any bots.
docker exec -it crowdsec cscli alerts list
╭────┬───────────────────┬───────────────────────────────────────┬─────────┬─────────────────────────────────────────────┬───────────┬─────────────────────────────────────────╮
│ ID │ value │ reason │ country │ as │ decisions │ created_at │
├────┼───────────────────┼───────────────────────────────────────┼─────────┼─────────────────────────────────────────────┼───────────┼─────────────────────────────────────────┤
│ 86 │ Ip:129.146.52.207 │ crowdsecurity/thinkphp-cve-2018-20062 │ US │ 31898 ORACLE-BMC-31898 │ ban:1 │ 2025-08-06 07:51:14.59356465 +0000 UTC │
│ 85 │ Ip:129.146.52.207 │ crowdsecurity/http-probing │ US │ 31898 ORACLE-BMC-31898 │ ban:1 │ 2025-08-06 07:50:27.497858459 +0000 UTC │
│ 84 │ Ip:129.146.52.207 │ crowdsecurity/CVE-2017-9841 │ US │ 31898 ORACLE-BMC-31898 │ ban:1 │ 2025-08-06 07:50:31.986742743 +0000 UTC │
│ 80 │ Ip:47.89.154.16 │ crowdsecurity/http-probing │ US │ 45102 Alibaba US Technology Co., Ltd. │ ban:1 │ 2025-08-06 02:14:47.399363246 +0000 UTC │
│ 78 │ Ip:43.166.136.113 │ crowdsecurity/thinkphp-cve-2018-20062 │ US │ 132203 Tencent Building, Kejizhongyi Avenue │ ban:1 │ 2025-08-06 01:28:25.249998707 +0000 UTC │
│ 77 │ Ip:43.166.136.113 │ crowdsecurity/http-probing │ US │ 132203 Tencent Building, Kejizhongyi Avenue │ ban:1 │ 2025-08-06 01:27:55.834522627 +0000 UTC │
│ 76 │ Ip:43.166.136.113 │ crowdsecurity/CVE-2017-9841 │ US │ 132203 Tencent Building, Kejizhongyi Avenue │ ban:1 │ 2025-08-06 01:28:08.746875988 +0000 UTC │
│ 74 │ Ip:8.222.225.103 │ crowdsecurity/thinkphp-cve-2018-20062 │ SG │ 45102 Alibaba US Technology Co., Ltd. │ ban:1 │ 2025-08-05 22:04:44.325377414 +0000 UTC │
│ 73 │ Ip:8.222.225.103 │ crowdsecurity/http-probing │ SG │ 45102 Alibaba US Technology Co., Ltd. │ ban:1 │ 2025-08-05 22:04:30.396505319 +0000 UTC │
│ 72 │ Ip:8.222.225.103 │ crowdsecurity/CVE-2017-9841 │ SG │ 45102 Alibaba US Technology Co., Ltd. │ ban:1 │ 2025-08-05 22:04:34.878409129 +0000 UTC │
│ 69 │ Ip:20.163.10.187 │ crowdsecurity/http-cve-probing │ US │ 8075 MICROSOFT-CORP-MSN-AS-BLOCK │ ban:1 │ 2025-08-05 19:22:47.360100531 +0000 UTC │
│ 67 │ Ip:20.163.15.225 │ crowdsecurity/CVE-2022-41082 │ US │ 8075 MICROSOFT-CORP-MSN-AS-BLOCK │ ban:1 │ 2025-08-05 15:56:51.234726408 +0000 UTC │
│ 56 │ Ip:23.140.40.74 │ crowdsecurity/thinkphp-cve-2018-20062 │ RU │ 61400 Start LLC │ ban:1 │ 2025-08-04 21:01:55.536820464 +0000 UTC │
│ 55 │ Ip:23.140.40.74 │ crowdsecurity/http-probing │ RU │ 61400 Start LLC │ ban:1 │ 2025-08-04 21:01:44.946667866 +0000 UTC │
│ 54 │ Ip:23.140.40.74 │ crowdsecurity/CVE-2017-9841 │ RU │ 61400 Start LLC │ ban:1 │ 2025-08-04 21:01:48.880039776 +0000 UTC │
╰────┴───────────────────┴───────────────────────────────────────┴─────────┴─────────────────────────────────────────────┴───────────┴─────────────────────────────────────────╯
🎉 It works! The table generated by cscli shows a few IP addresses have been banned.
✅ Done.