railbird-sf: add DuckDNS dynamic-DNS updater

Keep rocket-sense.duckdns.org pointed at this node's current public IP via
a systemd oneshot + 5min timer that hits the DuckDNS update API with the
source IP auto-detected. The residential WAN IP changes after ISP
failover, which otherwise leaves the public hostname stale.

Adds the agenix-encrypted duckdns-token secret and its key grant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 14:12:39 -07:00
parent 3144fab895
commit f755db41f5
3 changed files with 98 additions and 0 deletions

View File

@@ -30,6 +30,11 @@
file = ../secrets/org-api-ssh-key.age;
mode = "0400"; # Restrictive permissions for SSH key
};
# DuckDNS token for the rocket-sense.duckdns.org dynamic-DNS updater below.
age.secrets.duckdns-token = {
file = ../secrets/duckdns-token.railbird-sf.age;
mode = "0400";
};
services.org-agenda-api-host = {
enable = true;
@@ -121,6 +126,40 @@
security.acme.certs."rocket-sense.duckdns.org".extraDomainNames = ["rbsf.tplinkdns.com"];
# Dynamic-DNS updater: keep rocket-sense.duckdns.org pointed at this node's
# current public IP. The residential WAN IP changes (e.g. after an ISP
# outage/failover), and without this the public hostname goes stale and the
# site becomes unreachable until manually re-pointed. Leaving ip= blank tells
# DuckDNS to detect the source IP of this request, so it always reflects
# whatever connection the node is currently egressing through.
systemd.services.duckdns = {
description = "DuckDNS updater for rocket-sense.duckdns.org";
after = ["network-online.target"];
wants = ["network-online.target"];
serviceConfig = {
Type = "oneshot";
DynamicUser = true;
LoadCredential = ["token:${config.age.secrets.duckdns-token.path}"];
};
script = ''
token="$(cat "$CREDENTIALS_DIRECTORY/token")"
resp="$(${pkgs.curl}/bin/curl -fsS --retry 3 --max-time 30 \
"https://www.duckdns.org/update?domains=rocket-sense&token=$token&ip=")"
echo "DuckDNS response: $resp"
[ "$resp" = "OK" ] || { echo "DuckDNS update failed"; exit 1; }
'';
};
systemd.timers.duckdns = {
description = "Periodic DuckDNS update for rocket-sense.duckdns.org";
wantedBy = ["timers.target"];
timerConfig = {
OnBootSec = "1min";
OnUnitActiveSec = "5min";
Persistent = true;
};
};
# Open the standard Syncthing sync/discovery ports on the host firewall.
# Note: you may still need router/NAT port-forwards for inbound access from the internet.
services.syncthing.openDefaultPorts = true;

View File

@@ -0,0 +1,58 @@
age-encryption.org/v1
-> ssh-ed25519 ZgrTqA dbWFiSzLK7bR7hT7GwIMNNvD8WnZreFfDrtcA+LY/BE
OzTFip9dHKtI7EKWa/ati8LwVJJbSE6G3t6QFg4G6KQ
-> ssh-ed25519 ZaBdSg VkV8WJLCadquI8PH6mlEhFyXnBBQLtBAyixQJqaOT2w
xhyXTY215Vd8rWzkq8qD4rcHu3JjJh4wRG2+NGoYLnQ
-> ssh-ed25519 MHZylw Mjfvi/elwLcWlNhCBfYulnV6DkYdM2en0uhJUXctfQk
EspZ5/1PRZAEatU/Jh/s77aizKfQSpQ9ulipwCZS0tk
-> ssh-ed25519 sIUg6g gMwhuJAx5lURu7nD3TeWw8v7n9Kmn8Na83J6iNye1U4
FYtpIENIwosPhD4fOd8hjvbAYIhbJM+y+XiSeBjGIIM
-> ssh-ed25519 TnanwQ ihbJQDgyaoiSq30PktEjTlcQlzrXNKVVVVAT29eSI2g
ap3GKydDNX4nZC6mBaGC0BGFN0TveqcFOu91bbFJ2rs
-> ssh-ed25519 cH4aug YUs7dBlvrWhWU3WYPvkN30ozOgqvoMGOz1R9TDiVJVg
cuA4RDIc9m4og0zm+dd+3zO7OnbnI4Iisc/HCWEgQts
-> ssh-ed25519 ggrAFQ o2eVPsHgVCD+3EnYTzjziiYeqViVTFlmTtpe0oGulgU
+C2Yd50lkcXhQRnIu12EyY6U3Mjf8hAZZ33saicTWBo
-> ssh-rsa gwJx0Q
xaIephd6lidO23X/owa9Xn2SBqJxjjiaMoot/gn9gygTl9/TwvQ6g/XBXbuDjLkn
LjSJ9cRoyYMbzv8z21rmtuNRibNCoMR75+zff4BMf6tPpzdrtVAw05edwYp5hxtg
G9kv70WiLLah11Nn8ebluH1tMjbO4LKhy5chomielQ8yk27XBzRZS8M3Plmy2+za
AlUeUOsk5ExYl4jjbkz6RsHSryThbQjrcEBBYT3MVFzE4btiNsdCVsW5qfugZLnh
c9MSPzeDk5IrfNOZ35ZeFVUDkyrNuHno4VZKLp07I9mRPqLvvkOaAtIEcBag8agd
W/D5O3idVNc64/tp4icckw
-> ssh-ed25519 YFIoHA 7SE4NKISDqbR1aJmfLy3hXkPg12kUbiMShQ3hx+zOUw
SGQOn5t7dtt3e3+s4cvnoDGRjViQmDccFJWMF4HGsY8
-> ssh-ed25519 KQfiow LwUOPqeamtQ1i1RcxWEAtQDhHUny8kph4wmiRuPUnHs
q7VAs48mMeonKEn8IdX97CgxOHjRbY0nIYUbOmmysII
-> ssh-ed25519 kScIxg 4bEfbh/SnGsW8Kblr6Pzap1oobu+V+UsTeLHl4UAURs
7/t8Mo1jpr8650s3w/4L9FEfHLR/sH90xsBWKVzI2As
-> ssh-ed25519 HzX1zw qhd1iJVv4s7TbLAfVTqUTxxBaDsccDM/ypApAZcY5mE
oanEYt0MN3pcemOUMX+8sE7XxHW8h/IZ4dxaBJXh8Pk
-> ssh-ed25519 KQfiow IBfrzlbi++0rIa3A33S4gaFpBSOGAszqxeeFKLWpBjo
yBB/8ZnsIe4lPIR0vRLDpuFFLFgSxcryV1KCON257fw
-> ssh-ed25519 1o2X0w xUUvBiLbre/LWzHzMBbfE5unuO3UhGa7OcDi6rPy2Q8
KEuttdevVM3gPbea8xwq375qwf8TtrBPt3DeUUNH3EY
-> ssh-ed25519 KQ5iUA +h6kpwlMYc/Q4NbKpEsRCA6RjPpN5D1aV7eZeDFBDkk
Frm74ENiTFXBu/CjGYT0jRuPVO9qgHw5zoV/TbIOjUo
-> ssh-ed25519 AKGkDw aE1oI3u8LrY3XMymyoQUU5r5MyGDSpSMbcHtEZShwiU
gY9V3AbkonUl4sLcdfwaUFe0xKQIRxsFv2fq8VG+U0M
-> ssh-ed25519 0eS5+A zdhSEeS0/gD5WGTCmPK3jVraO/jzgRhwD0/qOHpz8GQ
QldMBke5zXTlVodSeKF0B8MTzxYipQ9cX6/VoaEZVp4
-> ssh-ed25519 9/4Prw JgMakHMTZyE7BNepkgTOGeMxkKcWEmUcq1CRm+IscyI
A/0EJhNWgofrESAaH3x12QpQujJ/bIIm9iGdGb9KQGg
-> ssh-ed25519 gAk3+Q EEorMwdbnlr3BsEdu9gg7jzivtnqzEPgGwZokSxGG2g
d9pq1Rbz685QKjcEF0KH3mIKwIzwZIVFkl+ujtT20rU
-> ssh-ed25519 X6eGtQ 7fWfLj6hTNYj1BxGPHlrcV9LdV75hYAAr+HXRT2GwWU
PjfOl8mGOhxWRfIGSiZ0l8Z/MXO8P1iBQnBUF+bLLl0
-> ssh-ed25519 0ma8Cw /FJJIBd2qfD2eNrXfDvd6cXxGeXbbYwSQktjyL1THQE
Q/wv7dasqq5yXe6RQdlsfgqW4ibxPHoreee1xBHi5Gs
-> ssh-ed25519 Tp0Z1Q fxdPsJ9c73yp6cU9UdAHX9YfF29U//wUCAucCBE44V4
lfTkurTHTanPAO3VbYoGRYHoWZ+BlwIoHE8lhVeRqXk
-> ssh-ed25519 ePNWZQ Qmc1C1keGmWgob+Xjh4kaVPAtDk6rG8PEvtdWFlbuxU
vKVO5t6TK4aWvMPw3O8ALq2glenVHEGFEGtYgsGf0k8
-> ssh-ed25519 hILzzA DgZif0MMn8xBc9b8qi0IrjsUAFrZW79Rx5aRIRQNXn4
ULkpXlqF0/zEl/8urDRipv9ErbRhxXE1ipojjuu6+sI
-> ssh-ed25519 qQi7yA EfyNuGZmnrRF+ikVpmaeyLRkiUzYsjNfovMlq1acTj0
0LuL347kis91fjiCOFpViIoO92cp5dFXJ22CL1GMJes
--- TwXaFhbbk8goUF8gbDnISLXupN+vmC9PgXoadMcVDaY
<EFBFBD><EFBFBD>ϙCI<EFBFBD><EFBFBD>I<EFBFBD><EFBFBD><EFBFBD>kQ<EFBFBD>w<EFBFBD><EFBFBD>BU'<27><><EFBFBD>5<EFBFBD><35><EFBFBD><EFBFBD>CTX<54><58><EFBFBD>GU><1A>i<EFBFBD>8}<7D>;<3B><><EFBFBD>[%d<>;#<23>)<29>)<29>.$<24>[F

View File

@@ -18,6 +18,7 @@ in {
"1896Folsom-k3s-token.age".publicKeys = keys.agenixKeys ++ keys.railbird-sf;
"api_service_account_key.json.age".publicKeys = keys.agenixKeys;
"k3s-registry.yaml.age".publicKeys = keys.agenixKeys ++ keys.railbird-sf;
"duckdns-token.railbird-sf.age".publicKeys = keys.agenixKeys ++ keys.railbird-sf;
"discourse-admin-password.age".publicKeys = keys.hostKeys;
"discourse-secret-key-base.age".publicKeys = keys.hostKeys;
"vaultwarden-environment-file.age".publicKeys = keys.hostKeys;