Files
web-log/container/site/content/posts/edge-reverse-proxy/index.md
T
nicholas 2e9bd0e409
Build & Push Hugo Site Image / Build & Push Image (push) Successful in 9s
Build & Push Hugo Site Image / deploy (push) Successful in 16s
add tags, descriptions to posts (#38)
2026-08-05 16:24:33 -05:00

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.