nix: sync atproto-likes module (non-root container, cache chown)
Upstream module dropped host-specific comments for its public repo, so the context that matters here — gateway fronting, why this vhost stays off the tailscale address group where opencode lives, and the exposure check script — moves into the copy's header where it belongs. Functional changes from upstream: the container now runs as uid 10001 and the unit chowns the bind-mounted cache before start (without it, cache files left by the earlier root container are unreadable and every cached actor 500s). 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
21dfa22f9a
commit
340e0d62a4
+42
-49
@@ -3,34 +3,34 @@
|
||||
# ⚠️ COPY. Canonical source:
|
||||
# /srv/project-orchestrator/projects/notplants-atproto/nix/atproto-likes.nix
|
||||
# Pure evaluation cannot import a path outside the flake tree. Re-copy after editing.
|
||||
#
|
||||
# Host-specific context that is deliberately NOT in the public project repo:
|
||||
# - the domain is fronted by the gateway (143.244.213.108); it forwards everything here
|
||||
# - this vhost lists 0.0.0.0 only so it never shares an nginx address group with
|
||||
# oc.commoninternet.net (opencode, on the tailscale address, no auth of its own)
|
||||
# - /srv/project-orchestrator/projects/notplants-atproto/tools/check-exposure.sh asserts
|
||||
# what this host serves publicly; run it after any nginx change
|
||||
# behind the host's nginx.
|
||||
#
|
||||
# Import this from the host configuration:
|
||||
# imports = [ /srv/project-orchestrator/projects/notplants-atproto/nix/atproto-likes.nix ];
|
||||
#
|
||||
# What it sets up:
|
||||
# * the Docker daemon (this host had none before)
|
||||
# * the Docker daemon
|
||||
# * a systemd unit that builds and runs docker-compose.yml from the project checkout
|
||||
# * an nginx vhost for atproto.commoninternet.net with a Let's Encrypt cert,
|
||||
# proxying to the container on 127.0.0.1:8731
|
||||
# * ports 80/443 open (they were closed — only 22 was public)
|
||||
# * ports 80/443 opened in the firewall
|
||||
#
|
||||
# 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. It must point at
|
||||
# DNS / TLS: point the domain's A record at this host (or at a reverse proxy that
|
||||
# forwards to it). ACME HTTP-01 needs the challenge to reach this nginx, so if a
|
||||
# proxy fronts the domain it must forward /.well-known/acme-challenge/ through.
|
||||
# Until a certificate is issued, NixOS installs a self-signed placeholder so nginx
|
||||
# still starts.
|
||||
#
|
||||
# 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.
|
||||
# A proxy in front should preserve the Host header (nginx routes by server_name),
|
||||
# pass Upgrade/Connection through (the loading page is a websocket) and allow a
|
||||
# long read timeout (a cold scan can run for minutes).
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
@@ -61,6 +61,14 @@ in
|
||||
WorkingDirectory = projectDir;
|
||||
# A cold `--build` pulls python:3.12-slim and installs pip deps.
|
||||
TimeoutStartSec = "1800";
|
||||
# The container runs unprivileged as uid 10001 (see Dockerfile), but the
|
||||
# page cache is a host bind mount, so its ownership is the host's business.
|
||||
# Without this the app 500s on any actor whose cache files were written by
|
||||
# an earlier root-running container.
|
||||
ExecStartPre = [
|
||||
"${pkgs.coreutils}/bin/mkdir -p ${projectDir}/cache"
|
||||
"${pkgs.coreutils}/bin/chown -R 10001:10001 ${projectDir}/cache"
|
||||
];
|
||||
ExecStart = "${compose} up -d --build --remove-orphans";
|
||||
ExecStop = "${compose} down";
|
||||
Restart = "on-failure";
|
||||
@@ -72,30 +80,18 @@ in
|
||||
enable = true;
|
||||
recommendedProxySettings = true;
|
||||
virtualHosts.${domain} = {
|
||||
# addSSL, NOT forceSSL. DNS for this domain points at the gateway
|
||||
# (143.244.213.108), which fronts this host — so the request that actually
|
||||
# arrives here is the gateway's back-end hop. If that hop is plain HTTP and
|
||||
# we answered with a 301 to https://atproto.commoninternet.net/, it would
|
||||
# resolve straight back to the gateway: a redirect loop. Serve both schemes
|
||||
# and let the gateway decide where TLS terminates.
|
||||
# addSSL, NOT forceSSL. If a reverse proxy fronts this domain and its
|
||||
# back-end hop is plain HTTP, a forced 301 to https://<domain>/ would
|
||||
# resolve straight back to that proxy — a redirect loop. Serving both
|
||||
# schemes lets whatever is in front decide where TLS terminates.
|
||||
addSSL = true;
|
||||
# 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;
|
||||
# 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.
|
||||
# Listens on 0.0.0.0 only, and the reject-everything default server below
|
||||
# owns those addresses. nginx groups server blocks by the connection's local
|
||||
# address, so keeping this vhost on the public address alone means it never
|
||||
# shares an address group with anything else the host may serve on another
|
||||
# interface — no other service can be reached by sending this one an
|
||||
# unexpected Host header.
|
||||
locations."/" = {
|
||||
proxyPass = "http://127.0.0.1:${toString hostPort}";
|
||||
# The loading page streams scan progress over a websocket.
|
||||
@@ -109,17 +105,14 @@ 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.
|
||||
# Strict default server for the public addresses. Without an explicit
|
||||
# default_server, nginx promotes the first server block in the group — so any
|
||||
# vhost added later silently becomes what answers unmatched, absent or spoofed
|
||||
# Host headers from the internet. This closes that door once: only names
|
||||
# deliberately served get a response.
|
||||
#
|
||||
# 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;
|
||||
@@ -134,9 +127,9 @@ in
|
||||
|
||||
security.acme = {
|
||||
acceptTerms = true;
|
||||
defaults.email = "mfowler.email@protonmail.com";
|
||||
defaults.email = "mfowler.email@protonmail.com"; # ACME contact
|
||||
};
|
||||
|
||||
# Public HTTP/HTTPS. Before this, only 22 was open on the public interface.
|
||||
# Public HTTP/HTTPS.
|
||||
networking.firewall.allowedTCPPorts = [ 80 443 ];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user