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
}
}
}