nixos server configurations

change pds handle domain to .starhaven.dev, not .pds.starhaven.dev

Changed files
+292 -22
secrets
kuribo
servers
+11 -2
secrets/kuribo/pds.env
···
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=ENC[AES256_GCM,data:thNijhgsq106+SJVnoseWu1S8SU2AB8Z5EqjKUzMBm+29FB146dmqPOphXL5yBPDuj0gjzFvfu4W7BOAKcx7fA==,iv:zcmhJopT8WHN2GfhDGO1oYp/NeyPpXeNrg6AVmDYMGk=,tag:JLcO4aVuKMVgRU6pirks+A==,type:str]
PDS_EMAIL_SMTP_URL=ENC[AES256_GCM,data:ltLt4Q7CaIL4swhDA2pBcMRR2gaMGcYw/7E7JtU4bMotEXrEO19V5ySomjbdFs3ImFzMtVVNY0am9R5Q40TZq85r6zDsoGv6,iv:Kh10CNUhkzqj5PROyFgGme0KUspZL/epxiQf2Ej0G6Y=,tag:noT7j38nip6OLbnwr8AWDQ==,type:str]
PDS_EMAIL_FROM_ADDRESS=ENC[AES256_GCM,data:VxEX3on/7jQ/SXmr53bvFzd3O/xu,iv:yehoA4hxkJ6UOjv625834otS1Es4uKtarjZjKFk2sJI=,tag:XaplSBikfq97mLTq+XyOrQ==,type:str]
+
PDS_BLOBSTORE_S3_BUCKET=ENC[AES256_GCM,data:a2jMY4b0HZEd,iv:IL4aG6cNOI3VLS+axwr47ZLPXQ8NAz4ZmtcHsVkYZE0=,tag:PedqBtofxgtARa4L81sMOw==,type:str]
+
PDS_BLOBSTORE_S3_REGION=ENC[AES256_GCM,data:jA5Fbg==,iv:oa73XQbCcMuYlSaDzs6TgAxe8QkofaIbTLjGLYViGtE=,tag:RaJcADDkLBNAeP9deMqFqA==,type:str]
+
PDS_BLOBSTORE_S3_ENDPOINT=ENC[AES256_GCM,data:f4Sg8Zb3KK7rbi7wxDvl05YcAq7FhejgbYRwQMNkzG8jxxk=,iv:RCN8LTqEb327n0ICipVerIAHXq/EzLsDx2uPIOg0VTc=,tag:/SV9uPuAr2S3O5rRDDqwRQ==,type:str]
+
PDS_BLOBSTORE_S3_ACCESS_KEY_ID=ENC[AES256_GCM,data:3xbCb2QO3x/jI4W+OXTuLcA18XE=,iv:Bs9knoz+PVZRbUIF1TnVP8bvP8xbN7ItVgQHsDDkQd4=,tag:4yqYBRGHUA8KurW19G9X8A==,type:str]
+
PDS_BLOBSTORE_S3_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:MWIwduFqqe20aYD7dGedcQskSQ6mAoDc55YvAjn1fYxzN6RCNgE28Q==,iv:C8NEL9+3UoN2uv8XG3Bpjedb1pans8g1YuyYdnQ918I=,tag:5hN28CsG67WOaL606k5Y6w==,type:str]
+
PDS_HCAPTCHA_SITE_KEY=ENC[AES256_GCM,data:qkhAjbyESlfzmeeNFBvE9OmM5bsbAG/lK0TFgJ0yRpPU8sEP,iv:3LL84eHS9A8c2FFovYi/g4+NroqLesbjIFhDnqLtEnc=,tag:EMPmyYSubtK/RJsa7GglqA==,type:str]
+
PDS_HCAPTCHA_SECRET_KEY=ENC[AES256_GCM,data:8eO1yMUYo3wnwe0z9n4WygNOpQPBmktbLukFSvAAOyC1Bic=,iv:5VllqLj5Wn0Lfj5X/Jobtk7qSRGm3rxIZuYFSyjkfJc=,tag:GQj6h6Fmzgj15LpQVKjUiQ==,type:str]
+
PDS_HCAPTCHA_TOKEN_SALT=ENC[AES256_GCM,data:no+3mQ==,iv:qXz3svuAPQFEkidBIT0xND+69UjlNbwVqbIM/2rl6rE=,tag:WIP/jBABy2+k5EAD6Mb7AA==,type:str]
+
CLOUDFLARE_API_TOKEN=ENC[AES256_GCM,data:1evMsnfzqoQtkFyzULhcKsmvOORGvRLUbUs1BvES7jQ99PFSUQ3B9w==,iv:6R40bMGDJOkD/OH9Cwzb6jjI1Syg90WvnVxHddd4yCU=,tag:U1sY/8m4VGOxh1IYQ/P3uQ==,type:str]
sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkenRnNWFlMzBIOTJsclYw\nSkw3ZW9pa0NRejRQd1FmOENTaE54UU4rcHkwCjFBSXljeTdQeGhXZDZrWS9JUkx0\nUXpxWUVKZTdGWjVLT1FRUmloMXhNdWcKLS0tIEhERVFJNU5pSU00b3MxUHB1Y280\nWTFiaTh0YXJyUXFKNGNrOE84elRONVUK20OPeWSZW2A9mTnEDfQmDc7n3jvUQhxb\nBatl6b0ismrkTWcRJK8nxImcvxBtMMCLfzK5Wt/9gBLJ6VDT6UPYFg==\n-----END AGE ENCRYPTED FILE-----\n
sops_age__list_0__map_recipient=age1h08rnd0jeddf55l6l3rf6dlwwh7mngcxy92tyz0hfysjqx4wvgrq6vmah2
sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4OVBaMzZ3UGd1R25SczNN\nd3R6bzVGTkN0SWpjb05wankvb2tpNTJvZlR3CjhwYUdHaTJ6Y1VPSTltOHhNbWdL\ncGs3OEJqaFljUFRhUVNncm13RFdETTgKLS0tIG1Mb1ZXQ3BpejdteWFkWUFyOGJu\ndFpyWkNiK2hoNlROd09xTzVueFdSUmcKDrIcoDDH2O/c9dyS/oLL0rudsrsmtOhJ\n55QagSzYouGlJbpl2xtBeUplg1WcEBX7FSW3UWFbz+Gc0/Rv76jRCA==\n-----END AGE ENCRYPTED FILE-----\n
sops_age__list_1__map_recipient=age1dhxleu7puseq4fz5gprzdssprdd452kjry2n47xaqfh22p5eyqfs68zysl
-
sops_lastmodified=2025-12-01T18:49:36Z
-
sops_mac=ENC[AES256_GCM,data:tSPG0g8XpTu0IJ8GQKIUczVlreLbZ/VFncomwSVzFEIXloJ6QQsX2hobyFCW4RwovQoZnVfO4uL8Ku/SIjsLIMCejLiGXAa4r0VDDZtxhnaX7tPBecG7gE3Ke15V4bT6B9uxB7TGhJYTsTlq/tb8D7UZG2+yWudFry8ArJRFxp0=,iv:9vxvakrxx8EmNBPSY1wnoV5cHx2/8GqGhNLYHyDj74w=,tag:5NeNRKenYykPj6b13bHFOg==,type:str]
+
sops_lastmodified=2025-12-04T01:38:29Z
+
sops_mac=ENC[AES256_GCM,data:M25RTOpBGfBh6jC194BJtfMKTC4MGpOtlHQS9JamLIWz2Zgo9P0Lp92UsUcEEVUI2r3aQ+MQZVeKxmmIDPcSC+DcMY1vKJwoPRFKTifHxf2mXfj3fB8mjHJm+k2DvMFhWQ5lic5mznx08kTISwdmTZqPMybzypxzLOhM/aZm37Y=,iv:Zm8KCNSYmVbz0+inrInCC3IV0AU2mOhcr06w7APK5Vw=,tag:QwkTAYZZXVEcWNe1XbjDAQ==,type:str]
sops_unencrypted_suffix=_unencrypted
sops_version=3.11.0
+215
servers/kuribo/ondemand_tls_helper.py
···
+
"""
+
This HTTP service implements the /tls-check endpoint used by Caddy's
+
on_demand_tls.ask configuration. Behavior:
+
+
- If the query parameter `domain` is ALLOWED -> return HTTP 200 OK with body "OK".
+
- Otherwise proxy the request (including the query string) to the real PDS
+
tls-check endpoint at http://127.0.0.1:<PDS_PORT>/tls-check and forward
+
the upstream status and body.
+
"""
+
+
import logging
+
import os
+
import socket
+
import sys
+
import urllib.error
+
import urllib.request
+
from http.server import BaseHTTPRequestHandler, HTTPServer
+
from urllib.parse import parse_qs, urlparse
+
+
# Configuration (can be overridden with env vars)
+
PDS_PORT = int(os.environ.get("PDS_PORT", "3000"))
+
LISTEN_HOST = os.environ.get("LISTEN_HOST", "127.0.0.1")
+
LISTEN_PORT = int(os.environ.get("LISTEN_PORT", "8081"))
+
TIMEOUT = float(os.environ.get("TIMEOUT", "5.0"))
+
+
# Allowed domain values (lowercase)
+
ALLOWED = {"pds", "knot", "spindle"}
+
+
# Configure logging to stderr (systemd/journal-friendly)
+
logging.basicConfig(
+
level=logging.INFO,
+
format="%(asctime)s %(levelname)s %(message)s",
+
stream=sys.stderr,
+
)
+
+
+
def filter_response_headers(headers):
+
"""
+
Given an iterable of (header, value) pairs or a mapping-like object,
+
return a dict with hop-by-hop headers removed. This avoids sending
+
problematic headers to the client (Caddy).
+
"""
+
hop_by_hop = {
+
"connection",
+
"keep-alive",
+
"proxy-authenticate",
+
"proxy-authorization",
+
"te",
+
"trailers",
+
"transfer-encoding",
+
"upgrade",
+
}
+
result = {}
+
if hasattr(headers, "items"):
+
iterator = headers.items()
+
else:
+
iterator = headers
+
for k, v in iterator:
+
if k.lower() not in hop_by_hop:
+
result[k] = v
+
return result
+
+
+
class TLSCheckHandler(BaseHTTPRequestHandler):
+
# Reduce console noise from BaseHTTPRequestHandler
+
def log_message(self, format, *args):
+
# route to logging module at INFO level
+
logging.info("%s - %s", self.client_address[0], format % args)
+
+
def _send(self, status, body=b"", headers=None):
+
# Send status, headers, and body to the client
+
self.send_response(status)
+
if headers:
+
for k, v in headers.items():
+
# BaseHTTPRequestHandler will fold multiple headers set via send_header
+
# if necessary; we assume simple string values here.
+
try:
+
self.send_header(k, v)
+
except Exception:
+
# Ignore any header-setting errors; continue to send response
+
logging.debug("skipping header %r due to error", k)
+
else:
+
self.send_header("Content-Type", "text/plain; charset=utf-8")
+
self.end_headers()
+
if body:
+
if isinstance(body, str):
+
body = body.encode("utf-8")
+
try:
+
self.wfile.write(body)
+
except BrokenPipeError:
+
# Client disconnected early; nothing to do
+
pass
+
+
def _proxy_to_pds(self, path_with_query):
+
"""
+
Proxy a request to http://127.0.0.1:<PDS_PORT><path_with_query>.
+
Returns (status, body_bytes, headers_dict).
+
"""
+
target = f"http://127.0.0.1:{PDS_PORT}{path_with_query}"
+
logging.debug("proxying to upstream: %s", target)
+
req = urllib.request.Request(
+
target, headers={"User-Agent": "ondemand-tls-helper/1.0"}
+
)
+
try:
+
with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
+
data = resp.read()
+
headers = filter_response_headers(resp.getheaders())
+
return resp.status, data, headers
+
except urllib.error.HTTPError as e:
+
# Upstream returned an HTTP error; return its body and status
+
try:
+
data = e.read()
+
except Exception:
+
data = b""
+
status = getattr(e, "code", 502)
+
headers = {}
+
logging.info("upstream returned HTTPError %s for %s", status, target)
+
return status, data, headers
+
except Exception as e:
+
logging.exception("error proxying to upstream %s: %s", target, e)
+
# Return 502 Bad Gateway
+
return 502, f"upstream error: {e}".encode("utf-8"), {}
+
+
def _get_domain_param(self):
+
parsed = urlparse(self.path)
+
qs = parse_qs(parsed.query)
+
domain_vals = qs.get("domain") or []
+
if not domain_vals:
+
return ""
+
return domain_vals[0].strip().lower()
+
+
def do_GET(self):
+
parsed = urlparse(self.path)
+
path_with_query = parsed.path + ("?" + parsed.query if parsed.query else "")
+
domain = self._get_domain_param()
+
+
if domain in ALLOWED:
+
logging.debug("allowed domain %r -> returning 200", domain)
+
return self._send(200, "OK")
+
+
status, body, headers = self._proxy_to_pds(path_with_query)
+
return self._send(status, body, headers=headers)
+
+
def do_HEAD(self):
+
parsed = urlparse(self.path)
+
path_with_query = parsed.path + ("?" + parsed.query if parsed.query else "")
+
domain = self._get_domain_param()
+
+
if domain in ALLOWED:
+
logging.debug("allowed domain (HEAD) %r -> returning 200", domain)
+
return self._send(200, b"")
+
+
status, _, headers = self._proxy_to_pds(path_with_query)
+
return self._send(status, b"", headers=headers)
+
+
+
def run():
+
server_address = (LISTEN_HOST, LISTEN_PORT)
+
try:
+
httpd = HTTPServer(server_address, TLSCheckHandler)
+
except OSError as e:
+
logging.error("cannot bind to %s:%s: %s", LISTEN_HOST, LISTEN_PORT, e)
+
sys.exit(1)
+
+
sa = httpd.socket.getsockname()
+
logging.info("ondemand TLS helper listening on %s:%s", sa[0], sa[1])
+
try:
+
httpd.serve_forever()
+
except KeyboardInterrupt:
+
logging.info("shutting down on keyboard interrupt")
+
except Exception:
+
logging.exception("server crashed")
+
finally:
+
try:
+
httpd.server_close()
+
except Exception:
+
pass
+
+
+
if __name__ == "__main__":
+
# Allow override of config via arguments if invoked with simple flags:
+
# --pds-port N --listen-host HOST --listen-port N --timeout S
+
# to keep the script flexible for local testing.
+
args = sys.argv[1:]
+
it = iter(args)
+
while True:
+
try:
+
a = next(it)
+
except StopIteration:
+
break
+
if a in ("--pds-port",):
+
try:
+
PDS_PORT = int(next(it))
+
except StopIteration:
+
break
+
elif a in ("--listen-host",):
+
try:
+
LISTEN_HOST = next(it)
+
except StopIteration:
+
break
+
elif a in ("--listen-port",):
+
try:
+
LISTEN_PORT = int(next(it))
+
except StopIteration:
+
break
+
elif a in ("--timeout",):
+
try:
+
TIMEOUT = float(next(it))
+
except StopIteration:
+
break
+
else:
+
# ignore unknown args
+
continue
+
+
run()
+66 -6
servers/kuribo/pds.nix
···
-
{ config, ... }:
+
{ config, pkgs, ... }:
let
pdsSettings = config.services.bluesky-pds.settings;
in
···
enable = true;
environmentFiles = [ config.sops.secrets.pds.path ];
settings = {
+
# https://github.com/bluesky-social/atproto/blob/main/packages/pds/src/config/env.ts
+
PDS_PORT = 3000;
PDS_HOSTNAME = "pds.starhaven.dev";
-
PDS_ADMIN_EMAIL = "admin@starhaven.dev";
+
PDS_CONTACT_EMAIL_ADDRESS = "admin@starhaven.dev";
+
PDS_SERVICE_HANDLE_DOMAINS = ".starhaven.dev";
+
+
# Branding
+
PDS_SERVICE_NAME = "\"Star Haven\"";
+
PDS_HOME_URL = "https://starhaven.dev";
+
#PDS_LOGO_URL
+
PDS_PRIMARY_COLOR = "#dbb23e";
+
PDS_PRIMARY_COLOR_CONTRAST = "#000";
+
+
# S3 is configured in secrets
+
PDS_BLOBSTORE_DISK_LOCATION = null;
};
};
services.caddy = {
enable = true;
+
package = pkgs.caddy.withPlugins {
+
plugins = [ "github.com/caddy-dns/cloudflare@v0.2.2" ];
+
hash = "sha256-ea8PC/+SlPRdEVVF/I3c1CBprlVp1nrumKM5cMwJJ3U=";
+
};
email = pdsSettings.PDS_ADMIN_EMAIL;
globalConfig = ''
on_demand_tls {
-
ask http://127.0.0.1:${toString pdsSettings.PDS_PORT}/tls-check
+
ask http://127.0.0.1:8081
}
+
+
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
'';
-
virtualHosts.${pdsSettings.PDS_HOSTNAME} = {
-
serverAliases = [ "*.${pdsSettings.PDS_HOSTNAME}" ];
+
virtualHosts."*.starhaven.dev" = {
extraConfig = ''
tls {
on_demand
}
-
reverse_proxy http://127.0.0.1:${toString pdsSettings.PDS_PORT}
+
handle / {
+
redir https://starhaven.dev
+
}
+
+
@knot host ${toString config.services.tangled.knot.server.hostname}
+
handle @knot {
+
reverse_proxy http://${toString config.services.tangled.knot.server.listenAddr}
+
}
+
+
@spindle host ${toString config.services.tangled.spindle.server.hostname}
+
handle @spindle {
+
reverse_proxy http://${toString config.services.tangled.spindle.server.listenAddr}
+
}
handle /xrpc/app.bsky.unspecced.getAgeAssuranceState {
header content-type "application/json"
···
header access-control-allow-origin "*"
respond `{"lastInitiatedAt":"2025-07-14T14:22:43.912Z","status":"assured"}` 200
}
+
+
handle {
+
reverse_proxy http://127.0.0.1:${toString pdsSettings.PDS_PORT}
+
}
'';
+
};
+
};
+
systemd.services.caddy = {
+
after = [
+
"ondemand-tls-helper.service"
+
"sops-nix.service"
+
];
+
serviceConfig.EnvironmentFile = config.sops.secrets.pds.path;
+
};
+
+
environment.etc."ondemand_tls_helper.py" = {
+
source = ./ondemand_tls_helper.py;
+
mode = "0755";
+
};
+
+
systemd.services.ondemand-tls-helper = {
+
description = "On-demand TLS helper for Caddy (returns 200 for allowed domains or proxies to PDS)";
+
wantedBy = [ "multi-user.target" ];
+
after = [ "network.target" ];
+
+
serviceConfig = {
+
ExecStart = "${pkgs.python3}/bin/python3 /etc/ondemand_tls_helper.py";
+
Environment = "PDS_PORT=${toString pdsSettings.PDS_PORT}";
+
User = "nobody";
+
Restart = "always";
+
RestartSec = 5;
};
};
-14
servers/kuribo/tangled.nix
···
-
{ config, ... }:
let
owner = "did:plc:tjgdahiw3u2djgnigyqeummg";
in
···
inherit owner;
hostname = "spindle.starhaven.dev";
};
-
};
-
};
-
-
services.caddy.virtualHosts = {
-
${config.services.tangled.knot.server.hostname} = {
-
extraConfig = ''
-
reverse_proxy http://${toString config.services.tangled.knot.server.listenAddr}
-
'';
-
};
-
${config.services.tangled.spindle.server.hostname} = {
-
extraConfig = ''
-
reverse_proxy http://${toString config.services.tangled.spindle.server.listenAddr}
-
'';
};
};
}