nixos server configurations
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+292 -22
+11 -2
secrets/kuribo/pds.env
··· 3 3 PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=ENC[AES256_GCM,data:thNijhgsq106+SJVnoseWu1S8SU2AB8Z5EqjKUzMBm+29FB146dmqPOphXL5yBPDuj0gjzFvfu4W7BOAKcx7fA==,iv:zcmhJopT8WHN2GfhDGO1oYp/NeyPpXeNrg6AVmDYMGk=,tag:JLcO4aVuKMVgRU6pirks+A==,type:str] 4 4 PDS_EMAIL_SMTP_URL=ENC[AES256_GCM,data:ltLt4Q7CaIL4swhDA2pBcMRR2gaMGcYw/7E7JtU4bMotEXrEO19V5ySomjbdFs3ImFzMtVVNY0am9R5Q40TZq85r6zDsoGv6,iv:Kh10CNUhkzqj5PROyFgGme0KUspZL/epxiQf2Ej0G6Y=,tag:noT7j38nip6OLbnwr8AWDQ==,type:str] 5 5 PDS_EMAIL_FROM_ADDRESS=ENC[AES256_GCM,data:VxEX3on/7jQ/SXmr53bvFzd3O/xu,iv:yehoA4hxkJ6UOjv625834otS1Es4uKtarjZjKFk2sJI=,tag:XaplSBikfq97mLTq+XyOrQ==,type:str] 6 + PDS_BLOBSTORE_S3_BUCKET=ENC[AES256_GCM,data:a2jMY4b0HZEd,iv:IL4aG6cNOI3VLS+axwr47ZLPXQ8NAz4ZmtcHsVkYZE0=,tag:PedqBtofxgtARa4L81sMOw==,type:str] 7 + PDS_BLOBSTORE_S3_REGION=ENC[AES256_GCM,data:jA5Fbg==,iv:oa73XQbCcMuYlSaDzs6TgAxe8QkofaIbTLjGLYViGtE=,tag:RaJcADDkLBNAeP9deMqFqA==,type:str] 8 + PDS_BLOBSTORE_S3_ENDPOINT=ENC[AES256_GCM,data:f4Sg8Zb3KK7rbi7wxDvl05YcAq7FhejgbYRwQMNkzG8jxxk=,iv:RCN8LTqEb327n0ICipVerIAHXq/EzLsDx2uPIOg0VTc=,tag:/SV9uPuAr2S3O5rRDDqwRQ==,type:str] 9 + PDS_BLOBSTORE_S3_ACCESS_KEY_ID=ENC[AES256_GCM,data:3xbCb2QO3x/jI4W+OXTuLcA18XE=,iv:Bs9knoz+PVZRbUIF1TnVP8bvP8xbN7ItVgQHsDDkQd4=,tag:4yqYBRGHUA8KurW19G9X8A==,type:str] 10 + PDS_BLOBSTORE_S3_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:MWIwduFqqe20aYD7dGedcQskSQ6mAoDc55YvAjn1fYxzN6RCNgE28Q==,iv:C8NEL9+3UoN2uv8XG3Bpjedb1pans8g1YuyYdnQ918I=,tag:5hN28CsG67WOaL606k5Y6w==,type:str] 11 + PDS_HCAPTCHA_SITE_KEY=ENC[AES256_GCM,data:qkhAjbyESlfzmeeNFBvE9OmM5bsbAG/lK0TFgJ0yRpPU8sEP,iv:3LL84eHS9A8c2FFovYi/g4+NroqLesbjIFhDnqLtEnc=,tag:EMPmyYSubtK/RJsa7GglqA==,type:str] 12 + PDS_HCAPTCHA_SECRET_KEY=ENC[AES256_GCM,data:8eO1yMUYo3wnwe0z9n4WygNOpQPBmktbLukFSvAAOyC1Bic=,iv:5VllqLj5Wn0Lfj5X/Jobtk7qSRGm3rxIZuYFSyjkfJc=,tag:GQj6h6Fmzgj15LpQVKjUiQ==,type:str] 13 + PDS_HCAPTCHA_TOKEN_SALT=ENC[AES256_GCM,data:no+3mQ==,iv:qXz3svuAPQFEkidBIT0xND+69UjlNbwVqbIM/2rl6rE=,tag:WIP/jBABy2+k5EAD6Mb7AA==,type:str] 14 + CLOUDFLARE_API_TOKEN=ENC[AES256_GCM,data:1evMsnfzqoQtkFyzULhcKsmvOORGvRLUbUs1BvES7jQ99PFSUQ3B9w==,iv:6R40bMGDJOkD/OH9Cwzb6jjI1Syg90WvnVxHddd4yCU=,tag:U1sY/8m4VGOxh1IYQ/P3uQ==,type:str] 6 15 sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkenRnNWFlMzBIOTJsclYw\nSkw3ZW9pa0NRejRQd1FmOENTaE54UU4rcHkwCjFBSXljeTdQeGhXZDZrWS9JUkx0\nUXpxWUVKZTdGWjVLT1FRUmloMXhNdWcKLS0tIEhERVFJNU5pSU00b3MxUHB1Y280\nWTFiaTh0YXJyUXFKNGNrOE84elRONVUK20OPeWSZW2A9mTnEDfQmDc7n3jvUQhxb\nBatl6b0ismrkTWcRJK8nxImcvxBtMMCLfzK5Wt/9gBLJ6VDT6UPYFg==\n-----END AGE ENCRYPTED FILE-----\n 7 16 sops_age__list_0__map_recipient=age1h08rnd0jeddf55l6l3rf6dlwwh7mngcxy92tyz0hfysjqx4wvgrq6vmah2 8 17 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 9 18 sops_age__list_1__map_recipient=age1dhxleu7puseq4fz5gprzdssprdd452kjry2n47xaqfh22p5eyqfs68zysl 10 - sops_lastmodified=2025-12-01T18:49:36Z 11 - sops_mac=ENC[AES256_GCM,data:tSPG0g8XpTu0IJ8GQKIUczVlreLbZ/VFncomwSVzFEIXloJ6QQsX2hobyFCW4RwovQoZnVfO4uL8Ku/SIjsLIMCejLiGXAa4r0VDDZtxhnaX7tPBecG7gE3Ke15V4bT6B9uxB7TGhJYTsTlq/tb8D7UZG2+yWudFry8ArJRFxp0=,iv:9vxvakrxx8EmNBPSY1wnoV5cHx2/8GqGhNLYHyDj74w=,tag:5NeNRKenYykPj6b13bHFOg==,type:str] 19 + sops_lastmodified=2025-12-04T01:38:29Z 20 + sops_mac=ENC[AES256_GCM,data:M25RTOpBGfBh6jC194BJtfMKTC4MGpOtlHQS9JamLIWz2Zgo9P0Lp92UsUcEEVUI2r3aQ+MQZVeKxmmIDPcSC+DcMY1vKJwoPRFKTifHxf2mXfj3fB8mjHJm+k2DvMFhWQ5lic5mznx08kTISwdmTZqPMybzypxzLOhM/aZm37Y=,iv:Zm8KCNSYmVbz0+inrInCC3IV0AU2mOhcr06w7APK5Vw=,tag:QwkTAYZZXVEcWNe1XbjDAQ==,type:str] 12 21 sops_unencrypted_suffix=_unencrypted 13 22 sops_version=3.11.0
+215
servers/kuribo/ondemand_tls_helper.py
··· 1 + """ 2 + This HTTP service implements the /tls-check endpoint used by Caddy's 3 + on_demand_tls.ask configuration. Behavior: 4 + 5 + - If the query parameter `domain` is ALLOWED -> return HTTP 200 OK with body "OK". 6 + - Otherwise proxy the request (including the query string) to the real PDS 7 + tls-check endpoint at http://127.0.0.1:<PDS_PORT>/tls-check and forward 8 + the upstream status and body. 9 + """ 10 + 11 + import logging 12 + import os 13 + import socket 14 + import sys 15 + import urllib.error 16 + import urllib.request 17 + from http.server import BaseHTTPRequestHandler, HTTPServer 18 + from urllib.parse import parse_qs, urlparse 19 + 20 + # Configuration (can be overridden with env vars) 21 + PDS_PORT = int(os.environ.get("PDS_PORT", "3000")) 22 + LISTEN_HOST = os.environ.get("LISTEN_HOST", "127.0.0.1") 23 + LISTEN_PORT = int(os.environ.get("LISTEN_PORT", "8081")) 24 + TIMEOUT = float(os.environ.get("TIMEOUT", "5.0")) 25 + 26 + # Allowed domain values (lowercase) 27 + ALLOWED = {"pds", "knot", "spindle"} 28 + 29 + # Configure logging to stderr (systemd/journal-friendly) 30 + logging.basicConfig( 31 + level=logging.INFO, 32 + format="%(asctime)s %(levelname)s %(message)s", 33 + stream=sys.stderr, 34 + ) 35 + 36 + 37 + def filter_response_headers(headers): 38 + """ 39 + Given an iterable of (header, value) pairs or a mapping-like object, 40 + return a dict with hop-by-hop headers removed. This avoids sending 41 + problematic headers to the client (Caddy). 42 + """ 43 + hop_by_hop = { 44 + "connection", 45 + "keep-alive", 46 + "proxy-authenticate", 47 + "proxy-authorization", 48 + "te", 49 + "trailers", 50 + "transfer-encoding", 51 + "upgrade", 52 + } 53 + result = {} 54 + if hasattr(headers, "items"): 55 + iterator = headers.items() 56 + else: 57 + iterator = headers 58 + for k, v in iterator: 59 + if k.lower() not in hop_by_hop: 60 + result[k] = v 61 + return result 62 + 63 + 64 + class TLSCheckHandler(BaseHTTPRequestHandler): 65 + # Reduce console noise from BaseHTTPRequestHandler 66 + def log_message(self, format, *args): 67 + # route to logging module at INFO level 68 + logging.info("%s - %s", self.client_address[0], format % args) 69 + 70 + def _send(self, status, body=b"", headers=None): 71 + # Send status, headers, and body to the client 72 + self.send_response(status) 73 + if headers: 74 + for k, v in headers.items(): 75 + # BaseHTTPRequestHandler will fold multiple headers set via send_header 76 + # if necessary; we assume simple string values here. 77 + try: 78 + self.send_header(k, v) 79 + except Exception: 80 + # Ignore any header-setting errors; continue to send response 81 + logging.debug("skipping header %r due to error", k) 82 + else: 83 + self.send_header("Content-Type", "text/plain; charset=utf-8") 84 + self.end_headers() 85 + if body: 86 + if isinstance(body, str): 87 + body = body.encode("utf-8") 88 + try: 89 + self.wfile.write(body) 90 + except BrokenPipeError: 91 + # Client disconnected early; nothing to do 92 + pass 93 + 94 + def _proxy_to_pds(self, path_with_query): 95 + """ 96 + Proxy a request to http://127.0.0.1:<PDS_PORT><path_with_query>. 97 + Returns (status, body_bytes, headers_dict). 98 + """ 99 + target = f"http://127.0.0.1:{PDS_PORT}{path_with_query}" 100 + logging.debug("proxying to upstream: %s", target) 101 + req = urllib.request.Request( 102 + target, headers={"User-Agent": "ondemand-tls-helper/1.0"} 103 + ) 104 + try: 105 + with urllib.request.urlopen(req, timeout=TIMEOUT) as resp: 106 + data = resp.read() 107 + headers = filter_response_headers(resp.getheaders()) 108 + return resp.status, data, headers 109 + except urllib.error.HTTPError as e: 110 + # Upstream returned an HTTP error; return its body and status 111 + try: 112 + data = e.read() 113 + except Exception: 114 + data = b"" 115 + status = getattr(e, "code", 502) 116 + headers = {} 117 + logging.info("upstream returned HTTPError %s for %s", status, target) 118 + return status, data, headers 119 + except Exception as e: 120 + logging.exception("error proxying to upstream %s: %s", target, e) 121 + # Return 502 Bad Gateway 122 + return 502, f"upstream error: {e}".encode("utf-8"), {} 123 + 124 + def _get_domain_param(self): 125 + parsed = urlparse(self.path) 126 + qs = parse_qs(parsed.query) 127 + domain_vals = qs.get("domain") or [] 128 + if not domain_vals: 129 + return "" 130 + return domain_vals[0].strip().lower() 131 + 132 + def do_GET(self): 133 + parsed = urlparse(self.path) 134 + path_with_query = parsed.path + ("?" + parsed.query if parsed.query else "") 135 + domain = self._get_domain_param() 136 + 137 + if domain in ALLOWED: 138 + logging.debug("allowed domain %r -> returning 200", domain) 139 + return self._send(200, "OK") 140 + 141 + status, body, headers = self._proxy_to_pds(path_with_query) 142 + return self._send(status, body, headers=headers) 143 + 144 + def do_HEAD(self): 145 + parsed = urlparse(self.path) 146 + path_with_query = parsed.path + ("?" + parsed.query if parsed.query else "") 147 + domain = self._get_domain_param() 148 + 149 + if domain in ALLOWED: 150 + logging.debug("allowed domain (HEAD) %r -> returning 200", domain) 151 + return self._send(200, b"") 152 + 153 + status, _, headers = self._proxy_to_pds(path_with_query) 154 + return self._send(status, b"", headers=headers) 155 + 156 + 157 + def run(): 158 + server_address = (LISTEN_HOST, LISTEN_PORT) 159 + try: 160 + httpd = HTTPServer(server_address, TLSCheckHandler) 161 + except OSError as e: 162 + logging.error("cannot bind to %s:%s: %s", LISTEN_HOST, LISTEN_PORT, e) 163 + sys.exit(1) 164 + 165 + sa = httpd.socket.getsockname() 166 + logging.info("ondemand TLS helper listening on %s:%s", sa[0], sa[1]) 167 + try: 168 + httpd.serve_forever() 169 + except KeyboardInterrupt: 170 + logging.info("shutting down on keyboard interrupt") 171 + except Exception: 172 + logging.exception("server crashed") 173 + finally: 174 + try: 175 + httpd.server_close() 176 + except Exception: 177 + pass 178 + 179 + 180 + if __name__ == "__main__": 181 + # Allow override of config via arguments if invoked with simple flags: 182 + # --pds-port N --listen-host HOST --listen-port N --timeout S 183 + # to keep the script flexible for local testing. 184 + args = sys.argv[1:] 185 + it = iter(args) 186 + while True: 187 + try: 188 + a = next(it) 189 + except StopIteration: 190 + break 191 + if a in ("--pds-port",): 192 + try: 193 + PDS_PORT = int(next(it)) 194 + except StopIteration: 195 + break 196 + elif a in ("--listen-host",): 197 + try: 198 + LISTEN_HOST = next(it) 199 + except StopIteration: 200 + break 201 + elif a in ("--listen-port",): 202 + try: 203 + LISTEN_PORT = int(next(it)) 204 + except StopIteration: 205 + break 206 + elif a in ("--timeout",): 207 + try: 208 + TIMEOUT = float(next(it)) 209 + except StopIteration: 210 + break 211 + else: 212 + # ignore unknown args 213 + continue 214 + 215 + run()
+66 -6
servers/kuribo/pds.nix
··· 1 - { config, ... }: 1 + { config, pkgs, ... }: 2 2 let 3 3 pdsSettings = config.services.bluesky-pds.settings; 4 4 in ··· 14 14 enable = true; 15 15 environmentFiles = [ config.sops.secrets.pds.path ]; 16 16 settings = { 17 + # https://github.com/bluesky-social/atproto/blob/main/packages/pds/src/config/env.ts 18 + 17 19 PDS_PORT = 3000; 18 20 PDS_HOSTNAME = "pds.starhaven.dev"; 19 - PDS_ADMIN_EMAIL = "admin@starhaven.dev"; 21 + PDS_CONTACT_EMAIL_ADDRESS = "admin@starhaven.dev"; 22 + PDS_SERVICE_HANDLE_DOMAINS = ".starhaven.dev"; 23 + 24 + # Branding 25 + PDS_SERVICE_NAME = "\"Star Haven\""; 26 + PDS_HOME_URL = "https://starhaven.dev"; 27 + #PDS_LOGO_URL 28 + PDS_PRIMARY_COLOR = "#dbb23e"; 29 + PDS_PRIMARY_COLOR_CONTRAST = "#000"; 30 + 31 + # S3 is configured in secrets 32 + PDS_BLOBSTORE_DISK_LOCATION = null; 20 33 }; 21 34 }; 22 35 23 36 services.caddy = { 24 37 enable = true; 38 + package = pkgs.caddy.withPlugins { 39 + plugins = [ "github.com/caddy-dns/cloudflare@v0.2.2" ]; 40 + hash = "sha256-ea8PC/+SlPRdEVVF/I3c1CBprlVp1nrumKM5cMwJJ3U="; 41 + }; 25 42 email = pdsSettings.PDS_ADMIN_EMAIL; 26 43 globalConfig = '' 27 44 on_demand_tls { 28 - ask http://127.0.0.1:${toString pdsSettings.PDS_PORT}/tls-check 45 + ask http://127.0.0.1:8081 29 46 } 47 + 48 + acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN} 30 49 ''; 31 - virtualHosts.${pdsSettings.PDS_HOSTNAME} = { 32 - serverAliases = [ "*.${pdsSettings.PDS_HOSTNAME}" ]; 50 + virtualHosts."*.starhaven.dev" = { 33 51 extraConfig = '' 34 52 tls { 35 53 on_demand 36 54 } 37 55 38 - reverse_proxy http://127.0.0.1:${toString pdsSettings.PDS_PORT} 56 + handle / { 57 + redir https://starhaven.dev 58 + } 59 + 60 + @knot host ${toString config.services.tangled.knot.server.hostname} 61 + handle @knot { 62 + reverse_proxy http://${toString config.services.tangled.knot.server.listenAddr} 63 + } 64 + 65 + @spindle host ${toString config.services.tangled.spindle.server.hostname} 66 + handle @spindle { 67 + reverse_proxy http://${toString config.services.tangled.spindle.server.listenAddr} 68 + } 39 69 40 70 handle /xrpc/app.bsky.unspecced.getAgeAssuranceState { 41 71 header content-type "application/json" ··· 43 73 header access-control-allow-origin "*" 44 74 respond `{"lastInitiatedAt":"2025-07-14T14:22:43.912Z","status":"assured"}` 200 45 75 } 76 + 77 + handle { 78 + reverse_proxy http://127.0.0.1:${toString pdsSettings.PDS_PORT} 79 + } 46 80 ''; 81 + }; 82 + }; 83 + systemd.services.caddy = { 84 + after = [ 85 + "ondemand-tls-helper.service" 86 + "sops-nix.service" 87 + ]; 88 + serviceConfig.EnvironmentFile = config.sops.secrets.pds.path; 89 + }; 90 + 91 + environment.etc."ondemand_tls_helper.py" = { 92 + source = ./ondemand_tls_helper.py; 93 + mode = "0755"; 94 + }; 95 + 96 + systemd.services.ondemand-tls-helper = { 97 + description = "On-demand TLS helper for Caddy (returns 200 for allowed domains or proxies to PDS)"; 98 + wantedBy = [ "multi-user.target" ]; 99 + after = [ "network.target" ]; 100 + 101 + serviceConfig = { 102 + ExecStart = "${pkgs.python3}/bin/python3 /etc/ondemand_tls_helper.py"; 103 + Environment = "PDS_PORT=${toString pdsSettings.PDS_PORT}"; 104 + User = "nobody"; 105 + Restart = "always"; 106 + RestartSec = 5; 47 107 }; 48 108 }; 49 109
-14
servers/kuribo/tangled.nix
··· 1 - { config, ... }: 2 1 let 3 2 owner = "did:plc:tjgdahiw3u2djgnigyqeummg"; 4 3 in ··· 24 23 inherit owner; 25 24 hostname = "spindle.starhaven.dev"; 26 25 }; 27 - }; 28 - }; 29 - 30 - services.caddy.virtualHosts = { 31 - ${config.services.tangled.knot.server.hostname} = { 32 - extraConfig = '' 33 - reverse_proxy http://${toString config.services.tangled.knot.server.listenAddr} 34 - ''; 35 - }; 36 - ${config.services.tangled.spindle.server.hostname} = { 37 - extraConfig = '' 38 - reverse_proxy http://${toString config.services.tangled.spindle.server.listenAddr} 39 - ''; 40 26 }; 41 27 }; 42 28 }