Not remote_ip not working as expected when using "handle"

1. The problem I’m having:

I have multiple sites defined for internal use and at the top of each block in my Caddyfile I specify import always_private trusting that it will prevent my internal site from being accessible externally.

(always_private) {
        import always
        import iplock
}

The relevant block is the iplock one

(iplock) {
        @denied not remote_ip ::1 2001:db8::/64
        respond @denied "Access denied" 403
}

Recently I was verifying things worked externally by curling the site from outside my network, expecting a 403, but I got the content from the site as if iplock was not defined…

ha.example.com {
        import always_private
        header >X-Frame-Options "SAMEORIGIN"

        header >Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://brands.home-assistant.io https://basemaps.cartocdn.com https://github.com https://raw.githubusercontent.com https://www.zigbee2mqtt.io https://slsys.github.io; font-src 'self' data:; connect-src 'self' https://catalogue.nodered.org; frame-ancestors 'self'; form-action 'self'; upgrade-insecure-requests; block-all-mixed-content; base-uri 'none'"

        replace "https://brands.home-assistant.io" "/brands"

        handle_path /brands* {
                root * /var/www/home-assistant-brands/build
                try_files {path} {path}/
                file_server browse
        }

        handle {
                reverse_proxy * http://[2001:db8::c5f8]:8123 {
                        header_up Accept-Encoding identity
                }
        }
}

What I noticed is that in order to actually enforce my iplock, i have to change the top import to import always and in each handle* i have to import iplock.

Like this:

ha.example.com {
        import always
        header >X-Frame-Options "SAMEORIGIN"

        header >Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://brands.home-assistant.io https://basemaps.cartocdn.com https://github.com https://raw.githubusercontent.com https://www.zigbee2mqtt.io https://slsys.github.io; font-src 'self' data:; connect-src 'self' https://catalogue.nodered.org; frame-ancestors 'self'; form-action 'self'; upgrade-insecure-requests; block-all-mixed-content; base-uri 'none'"

        replace "https://brands.home-assistant.io" "/brands"

        handle_path /brands* {
                import iplock
                root * /var/www/home-assistant-brands/build
                try_files {path} {path}/
                file_server browse
        }

        handle {
                import iplock
                reverse_proxy * http://[2001:db8::c5f8]:8123 {
                        header_up Accept-Encoding identity
                }
        }
}

This feels counterintuitive to me and possibly risky if you don’t know about this behavior.

So my question really is: Is this intended behavior? Or a bug?

2. Error messages and/or full log output:

Not relevant to topic

3. Caddy version:

v2.7.6 h1:w0NymbG2m9PcvKWsrXO6EEkY9Ru4FJK8uQbYcev1p3A=

4. How I installed and ran Caddy:

I installed it from dnf, then used this Dockerfile

# syntax=docker/dockerfile:1
###############################################################################
FROM docker.io/golang:latest AS builder
WORKDIR /build/

RUN go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest

RUN xcaddy build \
        --with github.com/caddy-dns/route53 \
        --with github.com/caddyserver/replace-response

###############################################################################
FROM scratch AS export-stage
COPY --from=builder /build/caddy /caddy

And the command BUILDAH_FORMAT=docker podman build --net host --output caddy .

To get a modified build that I used to overwrite the one dnf gave me.

a. System environment:

AlmaLinux 9.3
systemd 252 (252-18.el9)
Podman Version: 4.6.1

b. Command:

caddy validate --config /etc/caddy/Caddyfile && \
systemctl reload caddy

c. Service/unit/compose file:

# /usr/lib/systemd/system/caddy.service
# caddy.service
#
# For using Caddy with a config file.
#
# Make sure the ExecStart and ExecReload commands are correct
# for your installation.
#
# See https://caddyserver.com/docs/install for instructions.
#
# WARNING: This service does not use the --resume flag, so if you
# use the API to make changes, they will be overwritten by the
# Caddyfile next time the service is restarted. If you intend to
# use Caddy's API to configure it, add the --resume flag to the
# `caddy run` command or use the caddy-api.service file instead.

[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target

[Service]
Type=notify
User=caddy
Group=caddy
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --force
TimeoutStopSec=5s
LimitNOFILE=1048576
LimitNPROC=512
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

d. My complete Caddy config:

I don’t want to post the entire config as it contains private information.

{
        email letsencrypt@example.com
        order replace after encode
}

(headers-referrer-policy) {
        header {
                ?Referrer-Policy "no-referrer"
        }
}

(mustheaders) {
        header {
                Strict-Transport-Security "max-age=31536000; includesubdomains; preload"
                ?X-Content-Type-Options "nosniff"
                ?X-Frame-Options "DENY"
                ?X-Xss-Protection "1; mode=block"
                -Server
        }
}

(iplock) {
        @denied not remote_ip ::1 2001:db8::/64
        respond @denied "Access denied" 403
}

(tlsconf) {
        tls {
                protocols tls1.3
                key_type p384
        }
}

(errorhandler) {
        handle_errors {
                import mustheaders
                header Content-Type text/html
                respond `<head>
        <style>
                section {
                        width: 50%;
                }
                img {
                        width: 100%;
                }
                table {
                        width: 100%;
                }
                td {
                        border: 1px solid;
                        padding: 0.2em;
                }
                @media (prefers-color-scheme: dark) {
                        html {
                                background: #1b1b1b;
                                color: #fff;
                        }
                        a {
                                color: #8cb4ff;
                        }
                }
        </style>
</head>
<body>
<section>
<h1>Caddy on {system.hostname}</h1>
<img alt="{err.status_code}" src="https://http.cat/{err.status_code}" />
<table>
        <tr>
                <td>Field</td>
                <td>Value</td>
        </td>
        <tr>
                <td>Hostname</td>
                <td>{http.request.host}</td>
        </td>
        <tr>
                <td>Upstream</td>
                <td>{upstream_hostport}</td>
        </td>
        <tr>
                <td>Status Code</td>
                <td>{err.status_code} - {err.status_text}</td>
        </td>
        <tr>
                <td>Message</td>
                <td>{err.message}</td>
        </td>
        <tr>
                <td>Trace</td>
                <td>{err.trace}</td>
        </td>
</table>
</section>
` {err.status_code}
        }
}

(errorhandler_public) {
        handle_errors {
                respond `Status Code: {err.status_code} - {err.status_text}
Hostname: {http.request.host}
` {err.status_code}
        }
}

(alwaysfiles) {
        respond "/robots.txt" 200 {
                body "User-agent: *
Disallow: /" #
                close
        }
}

(logging) {
        log {
                format filter {
                        wrap console
                        fields {
                                duration delete
                                request>headers delete
                                request>remote_ip ip_mask {
                                        ipv4 24
                                        ipv6 64
                                }
                                request>client_ip delete
                                request>remote_port delete
                                request>tls delete
                                request>uri delete
                                resp_headers delete
                                user_id delete
                        }
                }
        }
}

(always) {
        import tlsconf
        import mustheaders
        import headers-referrer-policy
        import errorhandler
        import alwaysfiles
        import logging
}

(always_private) {
        import always
        import iplock
}

(always_public) {
        import always
}

:80 {
        header ?Content-Type text/html
        respond `<h1>Hello {{.RemoteIP}}</h1>
<p>You have lost your way.</p>
<p>Connecting via HTTP like a caveman!</p>
` 404
}

ha.example.com {
        import always
        header >X-Frame-Options "SAMEORIGIN"

        header >Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://brands.home-assistant.io https://basemaps.cartocdn.com https://github.com https://raw.githubusercontent.com https://www.zigbee2mqtt.io https://slsys.github.io; font-src 'self' data:; connect-src 'self' https://catalogue.nodered.org; frame-ancestors 'self'; form-action 'self'; upgrade-insecure-requests; block-all-mixed-content; base-uri 'none'"

        replace "https://brands.home-assistant.io" "/brands"

        handle_path /brands* {
                import iplock
                root * /var/www/home-assistant-brands/build
                try_files {path} {path}/
                file_server browse
        }

        handle {
                import iplock
                reverse_proxy * http://[2001:db8::c5f8]:8123 {
                        header_up Accept-Encoding identity
                }
        }
}

5. Links to relevant resources:

That’s because respond is lower on the directive order than handle. See the docs: Caddyfile Directives — Caddy Documentation This means that all handle will be considered first.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.