nginx: strict public default server; keep oc off 0.0.0.0 (copy)
Sync of the canonical module. The atproto vhost listens on 0.0.0.0 only, so it never shares an address group with oc.commoninternet.net (which is the default server on 100.84.190.30:80 and has no auth in front of it). A new public-default-reject vhost owns the public addresses — return 444 on 80, ssl_reject_handshake on 443 — so unmatched, absent or spoofed Host headers from the internet get nothing, now and after any future vhost is added. The tailnet is untouched: oc.commoninternet.net still serves opencode there. Verified with tools/check-exposure.sh in the project repo — 9/9 ok. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SmEK2voMnBa23495aLk1Ce
This commit is contained in:
co-authored by
Claude Opus 5
parent
7d72c05ae7
commit
21dfa22f9a
+54
-28
@@ -1,9 +1,9 @@
|
||||
# atproto-likes — the "most-liked accounts" web UI, run as a docker-compose stack behind nginx.
|
||||
# atproto-likes — the "most-liked accounts" web UI, run as a docker-compose stack
|
||||
#
|
||||
# ⚠️ COPY. Canonical source:
|
||||
# /srv/project-orchestrator/projects/notplants-atproto/nix/atproto-likes.nix
|
||||
# A flake cannot import an absolute path outside its own tree under pure
|
||||
# evaluation, so the module is copied here. Re-copy after editing the original.
|
||||
# Pure evaluation cannot import a path outside the flake tree. Re-copy after editing.
|
||||
# behind the host's nginx.
|
||||
#
|
||||
# Import this from the host configuration:
|
||||
# imports = [ /srv/project-orchestrator/projects/notplants-atproto/nix/atproto-likes.nix ];
|
||||
@@ -18,13 +18,19 @@
|
||||
# DNS: atproto.commoninternet.net resolves to 143.244.213.108 — the operator's
|
||||
# gateway (tailnet peer `gateway-server-aug2-9pm`, 100.80.66.110), which fronts this
|
||||
# host rather than pointing at it directly. So what arrives here is the gateway's
|
||||
# back-end hop, and the gateway needs a route for this hostname to either
|
||||
# http://100.84.190.30 (tailnet, preferred)
|
||||
# http://168.119.126.100 (public)
|
||||
# preserving the Host header, passing Upgrade/Connection through (the loading page
|
||||
# is a websocket), and allowing a long read timeout (a cold scan runs minutes).
|
||||
# ACME here can only succeed if the gateway forwards /.well-known/acme-challenge/;
|
||||
# otherwise the gateway should own the certificate and `enableACME` can go false.
|
||||
# back-end hop. It must point at
|
||||
#
|
||||
# http://168.119.126.100 (the PUBLIC address — never 100.84.190.30)
|
||||
#
|
||||
# because the tailscale address is where the opencode UI lives and is that address
|
||||
# group's default server; a hop there with a missing or wrong Host header would be
|
||||
# served opencode. See the listen comments below. The gateway should also pass
|
||||
# Upgrade/Connection through (the loading page is a websocket) and allow a long
|
||||
# read timeout (a cold scan runs minutes).
|
||||
#
|
||||
# The gateway forwards everything, including /.well-known/acme-challenge/, so ACME
|
||||
# HTTP-01 here will start succeeding as soon as the gateway has a route for this
|
||||
# hostname. Until then nginx serves the self-signed placeholder NixOS installs.
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
@@ -73,25 +79,23 @@ in
|
||||
# resolve straight back to the gateway: a redirect loop. Serve both schemes
|
||||
# and let the gateway decide where TLS terminates.
|
||||
addSSL = true;
|
||||
# Keeps retrying; it can only succeed if the gateway forwards
|
||||
# /.well-known/acme-challenge/ here, since the A record is the gateway's.
|
||||
# Until then nginx uses the self-signed placeholder NixOS installs, and the
|
||||
# acme-atproto… unit sits in `failed`. Set this false if the gateway is to
|
||||
# own the certificate permanently.
|
||||
# The gateway forwards everything, so once it has a route for this hostname
|
||||
# the HTTP-01 challenge reaches us and this starts succeeding on its own.
|
||||
# Until then the acme-atproto… unit sits in `failed` and nginx uses the
|
||||
# self-signed placeholder; nothing else on the host is affected.
|
||||
enableACME = true;
|
||||
# The `oc.commoninternet.net` vhost binds explicitly to the tailscale IP on
|
||||
# port 80. nginx prefers the most specific listen address, so a request to
|
||||
# 100.84.190.30:80 was only ever matched against THAT server block — this
|
||||
# vhost, listening on 0.0.0.0, never got a look in, and a gateway hop over
|
||||
# the tailnet was served the opencode UI instead. Listing the tailscale
|
||||
# address explicitly here puts both vhosts on that socket, so server_name
|
||||
# decides, which is what we want.
|
||||
listen = [
|
||||
{ addr = "0.0.0.0"; port = 80; ssl = false; }
|
||||
{ addr = "0.0.0.0"; port = 443; ssl = true; }
|
||||
{ addr = "100.84.190.30"; port = 80; ssl = false; }
|
||||
{ addr = "100.84.190.30"; port = 443; ssl = true; }
|
||||
];
|
||||
# PUBLIC INTERFACE ONLY — deliberately not the tailscale address.
|
||||
#
|
||||
# nginx groups servers by the connection's local address and prefers the most
|
||||
# specific listen. `oc.commoninternet.net` (the opencode UI) binds explicitly
|
||||
# to 100.84.190.30:80, so that address has its own group in which oc is the
|
||||
# DEFAULT server. Anything arriving there without a matching Host — a proxy
|
||||
# that drops the header, or sends `Host: 100.84.190.30` — is served opencode.
|
||||
# So the gateway must hop to the PUBLIC address, never the tailscale one:
|
||||
# this vhost lives on 0.0.0.0, where the only servers are itself and the
|
||||
# reject-everything default below. Keeping the two names on disjoint
|
||||
# addresses is what makes "opencode cannot leak publicly" structural rather
|
||||
# than a matter of getting a Host header right.
|
||||
locations."/" = {
|
||||
proxyPass = "http://127.0.0.1:${toString hostPort}";
|
||||
# The loading page streams scan progress over a websocket.
|
||||
@@ -104,6 +108,28 @@ in
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
# Strict default server for the PUBLIC addresses. Without an explicit
|
||||
# default_server, nginx promotes the first server block in the group — so
|
||||
# every future vhost added to 0.0.0.0 silently becomes the thing that answers
|
||||
# unmatched/absent/spoofed Host headers from the internet. This closes that
|
||||
# door once: only names we deliberately serve get a response here.
|
||||
#
|
||||
# port 80 -> 444 (close the connection, no response at all)
|
||||
# port 443 -> ssl_reject_handshake, so an unknown SNI never even gets TLS
|
||||
#
|
||||
# It listens only on 0.0.0.0, so the tailscale address group is untouched and
|
||||
# oc.commoninternet.net keeps behaving exactly as before on the tailnet.
|
||||
virtualHosts."public-default-reject" = {
|
||||
default = true;
|
||||
serverName = null;
|
||||
rejectSSL = true;
|
||||
listen = [
|
||||
{ addr = "0.0.0.0"; port = 80; ssl = false; }
|
||||
{ addr = "0.0.0.0"; port = 443; ssl = true; }
|
||||
];
|
||||
extraConfig = "return 444;";
|
||||
};
|
||||
};
|
||||
|
||||
security.acme = {
|
||||
|
||||
Reference in New Issue
Block a user