Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions nixos/beast/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,36 @@ This host is a storage/NAS node. The host-specific configuration lives in
- Snapper timelines and scheduled Btrfs scrubs for data hygiene.
- SMART monitoring for disk health.

## DDNS rollout (`jf.ihar.dev`, `au.ihar.dev`, `js.ihar.dev`)

`nixos/beast/default.nix` runs `services.ddclient` to update Dynu directly from
this host (instead of router-managed DDNS).

1. Create a Dynu hostname (current: `ihrachyshka-home.freeddns.org`).
1. In `nixos/beast/default.nix`, set:
- `dynuHostname = "ihrachyshka-home.freeddns.org";`
- `dynuUsername = "ihrachyshka";`
1. Add `ddns.dynu.password` to `secrets/beast.yaml` via `sops`.
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README refers to ddns.dynu.password, but the Nix config reads the secret from the SOPS key ddns/dynu/password (slash-separated path) via key = \"ddns/dynu/password\". To avoid confusion during rollout, align the documentation with the configured key path (or explicitly mention both notations and which tool expects which).

Suggested change
1. Add `ddns.dynu.password` to `secrets/beast.yaml` via `sops`.
1. Add SOPS key `ddns/dynu/password` (YAML path `ddns.dynu.password`) to `secrets/beast.yaml` via `sops`.

Copilot uses AI. Check for mistakes.
1. Rebuild on beast.
1. At your registrar DNS, repoint:
- `jf.ihar.dev CNAME <dynu-hostname>`
- `au.ihar.dev CNAME <dynu-hostname>`
- `js.ihar.dev CNAME <dynu-hostname>`

Validation:

```bash
dig +short jf.ihar.dev CNAME
dig +short au.ihar.dev CNAME
dig +short js.ihar.dev CNAME
dig +short ihrachyshka-home.freeddns.org A
systemctl status ddclient
journalctl -u ddclient -n 100 --no-pager
curl -I https://jf.ihar.dev
curl -I https://au.ihar.dev
curl -I https://js.ihar.dev
```

## Storage

- `/volume2` is a Btrfs filesystem mounted with `compress=zstd`, `noatime`, and
Expand Down
39 changes: 38 additions & 1 deletion nixos/beast/default.nix
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
config,
lib,
pkgs,
...
Expand All @@ -22,7 +23,9 @@ let
nfsPorts = [
2049 # nfsd
];
hasSopsSecrets = builtins.pathExists ../../secrets/beast.yaml;
# DDNS provider target for public endpoints (jf/au/js).
dynuHostname = "ihrachyshka-home.freeddns.org";
dynuUsername = "ihrachyshka";
in
{
imports = [
Expand Down Expand Up @@ -101,6 +104,7 @@ in
"render"
"video"
];
users.groups.ddclient-secrets = { };
systemd.services.jellyfin.unitConfig.RequiresMountsFor = "/media";

# Reverse proxy with automatic TLS.
Expand All @@ -109,6 +113,39 @@ in
defaults.email = "ihar.hrachyshka@gmail.com";
};

# Run DDNS updates from this host (instead of the router).
sops = {
defaultSopsFile = ../../secrets/beast.yaml;
age.keyFile = "/var/lib/sops-nix/key.txt";
useSystemdActivation = true;
secrets.ddnsDynuPassword = {
key = "ddns/dynu/password";
group = "ddclient-secrets";
mode = "0440";
};
};

services.ddclient = {
enable = true;
interval = "1min";
protocol = "dyndns2";
server = "api.dynu.com";
username = dynuUsername;
passwordFile = config.sops.secrets.ddnsDynuPassword.path;
domains = [ dynuHostname ];
ssl = true;
quiet = true;
usev4 = "webv4,webv4=checkip.dynu.com/,webv4-skip='IP Address'";
usev6 = "";
};
systemd.services.ddclient = {
wants = [ "sops-install-secrets.service" ];
after = [ "sops-install-secrets.service" ];
serviceConfig = {
SupplementaryGroups = [ "ddclient-secrets" ];
};
};

services.nginx = {
enable = true;
recommendedProxySettings = true;
Expand Down
19 changes: 17 additions & 2 deletions scripts/sops-bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ usage() {
cat <<'EOF'
Usage:
scripts/sops-bootstrap.sh HOST [--user USER]
scripts/sops-bootstrap.sh --help

This script:
1) SSHes into HOST and generates /var/lib/sops-nix/key.txt (if missing)
Expand Down Expand Up @@ -44,24 +45,38 @@ user="${USER:-$(whoami)}"

while [[ $# -gt 0 ]]; do
case "$1" in
-h | --help)
usage
exit 0
;;
--user)
if [[ $# -lt 2 || -z "${2:-}" ]]; then
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition $# -lt 2 is redundant when combined with -z \"${2:-}\". If $# is less than 2, then $2 will be unset and the second condition will be true. The check can be simplified to just -z \"${2:-}\".

Suggested change
if [[ $# -lt 2 || -z "${2:-}" ]]; then
if [[ -z "${2:-}" ]]; then

Copilot uses AI. Check for mistakes.
echo "Missing value for --user" >&2
usage >&2
exit 1
fi
user="$2"
shift 2
;;
-*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
*)
if [[ -z "$host" ]]; then
host="$1"
shift
else
usage
usage >&2
exit 1
fi
;;
esac
done

if [[ -z "$host" ]]; then
usage
usage >&2
exit 1
fi

Expand Down
12 changes: 11 additions & 1 deletion scripts/sops-cat.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ usage() {
cat <<'EOF'
Usage:
scripts/sops-cat.sh [HOST]
scripts/sops-cat.sh --help

Decrypt and print secrets/HOST.yaml.
If HOST is omitted, the current short hostname is used.
Expand All @@ -24,12 +25,21 @@ resolve_repo_root() {
host=""
while [[ $# -gt 0 ]]; do
case "$1" in
-h | --help)
usage
exit 0
;;
-*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
*)
if [[ -z "$host" ]]; then
host="$1"
shift
else
usage
usage >&2
exit 1
fi
;;
Expand Down
39 changes: 34 additions & 5 deletions scripts/sops-copy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ usage() {
cat <<'EOF'
Usage:
scripts/sops-copy.sh SRC_HOST DST_HOST KEY_PATH
scripts/sops-copy.sh --help

Copy KEY_PATH from secrets/SRC_HOST.yaml into secrets/DST_HOST.yaml.
Example:
Expand Down Expand Up @@ -86,14 +87,42 @@ path_to_jq_array() {
}

main() {
if [[ $# -ne 3 ]]; then
usage
local src_host=""
local dst_host=""
local key_path=""
Comment on lines +90 to +92
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These variable declarations are redundant since they're immediately assigned empty strings and then conditionally populated in the loop. Remove the explicit empty string assignments and rely on bash's default unset variable behavior in the conditional checks.

Suggested change
local src_host=""
local dst_host=""
local key_path=""
local src_host
local dst_host
local key_path

Copilot uses AI. Check for mistakes.

while [[ $# -gt 0 ]]; do
case "$1" in
-h | --help)
usage
exit 0
;;
-*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
*)
if [[ -z "$src_host" ]]; then
src_host="$1"
elif [[ -z "$dst_host" ]]; then
dst_host="$1"
elif [[ -z "$key_path" ]]; then
key_path="$1"
else
usage >&2
exit 1
fi
shift
;;
esac
done

if [[ -z "$src_host" || -z "$dst_host" || -z "$key_path" ]]; then
usage >&2
exit 1
fi

local src_host="$1"
local dst_host="$2"
local key_path="$3"
local key_expr
local key_path_array
key_expr="$(path_to_yq_expr "$key_path")"
Expand Down
12 changes: 11 additions & 1 deletion scripts/sops-edit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ usage() {
cat <<'EOF'
Usage:
scripts/sops-edit.sh [HOST]
scripts/sops-edit.sh --help
If HOST is omitted, the current short hostname is used.
If a template exists for HOST, merge it into the secret first.
Expand All @@ -27,12 +28,21 @@ resolve_repo_root() {
repo_root="$(resolve_repo_root)"
while [[ $# -gt 0 ]]; do
case "$1" in
-h | --help)
usage
exit 0
;;
-*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
*)
if [[ -z "$host" ]]; then
host="$1"
shift
else
usage
usage >&2
exit 1
fi
;;
Expand Down
10 changes: 10 additions & 0 deletions scripts/sops-update.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ usage() {
cat <<'EOF'
Usage:
scripts/sops-update.sh [HOST]
scripts/sops-update.sh --help

Update secrets/HOST.yaml from template defaults in secrets/_template.yaml.

Expand All @@ -27,6 +28,15 @@ main() {
host=""
while [[ $# -gt 0 ]]; do
case "$1" in
-h | --help)
usage
exit 0
;;
-*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
*)
if [[ -z "$host" ]]; then
host="$1"
Expand Down
4 changes: 4 additions & 0 deletions secrets/_template.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
attic:
token: "REPLACE_ME"

ddns:
dynu:
password: "REPLACE_ME"
29 changes: 16 additions & 13 deletions secrets/beast.yaml
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
attic:
token: ENC[AES256_GCM,data:vT37QsyT7rwDPi61CHe4u7HuRcPhZd7b8Bgsl2vzeBsnYUmimDu6yw7AU2fL61/0oizzTMoLeoOohLejQK3Vy2MWBqwicKNaEt75TSWh5PZDqUpW1t/ziICgcwLQSL/UIMPBmLjfi+npuax6VDtF23BMkR14Z5IQhZCPUZDJcqgP8jp9WXlYaJ5HYFnNxO1z9ELJzd7IUk+DhjSJvZ8G2224X6/zqZC1KMmg6voDUSR+pSgm3kAJqWqfOoYFvbjHYk0BPzL4KhmJzpMx7p/Yb7moBA6mv2djydp52G0TfU7RRvhgpSIWXVE0DO1GfdSve3tOJjiqWzOb9lPE3Oz2ENu+h9KVAzrFigIsEWZntjXxFEd5DGX6ITMNiRHARXCNyZJ2T9XkGsqAiXN9h0yAT4HqEChfXIsErh8D+gutxFIqjKWYXiqz60arcYmm7X2+9qf1xo06rF5Sur7+Ig8FEBkT4vgRpM3Cod57Ufzb/OJy36De+A6+zpjTPWItuO9D7jW0j/qUFvXi8oIEPVLscy6/RR+gXEphe81LGDdjPko3TY1YX5NfmiiousM5to+DeSz3FsxjA0VctOZSxfNb5NFCLtgOz/HhP37Z93Jw8HSm85Bmt3TVaqpGWvoISQNXJ+7I9EuiOkjmdcvgEDBqYGfETqdtT73pF3AIMxl3TU/mmipZQtZgbDwKFGl28P6vild8E4eh4ADkkW09166WF/vKq0Z5ivRNvBrzvTOEAwnN8eNjUXEI+tIWkyQXbKGR1KyqDQ+V6CcGb7rM405mBb7QFGKPpz87g2CNZetVsymK9f+lj1ogf6Fp2bXMlbyh2eoIfiPzRm1oDfcP88sNs5GulNwHqN07yVoJK1Bsyofb2nDJyMqAK+pZ/94Szo17hds6Omoqr5/HUcnqo3AkjKDjQKLhG3OFxM12F2VsGM6rOSh6jdadb/zulWOgf2uryvN1Cc06JQGb60Zwr2i+Q4Eu7I7gpN0Dym9Y4SQfEQnGPn1IfDm+khi3ShI2HewkZsaTIdaT7yybR2WddBQ8f2/ousVkrIxU3GtH/iuZMr22kAyG+hotEF8jNPf5r79AtWZWdWLoFvgw1/MbpqZ2Wigbta5Vv04F2DtkxykLavCdW3m16E/wLUBVqJMLGD3XQLH9x+MQvfoX,iv:HD/ODKYCseYrkVoFFPBi+ShEEXQCpwDruj9XLYV2/EQ=,tag:HDqIXN+DIqd6G+/qNtmlEA==,type:str]
token: ENC[AES256_GCM,data:ZgW3NcYg7yti/EHudju2I9eRShNNE8HnHh+hoCR1WBG7yxv7WZTqWa10W8TYSimJYIJiCuDiszWVibx5sqANdmaSgI7JV+NUpq8erwpTRkf5BGOa7vxLDvVPSUEJ+AuUQvLpqY2nCj/ErKpOTKsKn7jBKjaJ13qDZXhvwFMsn2KtOD4nu5VGo44H1hMUWC6OQNoXu1YxyS+jm+kKkPYxlcIEgYR/2PynSeoc7prwqG1veSfWH2S+APxW2fYjFHs4nyU9Gp5b5hUe7DgGmisKT/sMI1j2Odgma6uCRbgwBBUh6cMvbqJHvkSht2Qm1+o90E2ZEr0G/7EPavBVSPINmLQ+6jAN8fFcLUy1AUDAw108eh1uUjVFzjcn2DWfkWzoYEnEDOgapuQ5HJ41N5F+H/Hxtr8Y31EMahrp5gDq6JZ6tQPwBhDhqv04ZU+J6Sm9ReZNNahktqGRwjZ62R+Iupnfc2E5CUkjypq4Hwn0ARGU9psshjz1lL1yYppwHlQtx90F5KKo2jSo2xY+tuBoKJa/SbA2ih9Sb5LdtE7OpMTyQz6FDWA6KqL52O3gddJjoePNuBD2ivsGm+/sHUn0XeCrv1RVaaFRi10kWbIKL5nxtwHYcxDhkDHyE+c73P1DxnH3AkgRMoJ2sWYZQmybiDz86g7OqGcNYk28sjykP95YUf2XlE+NvAgR/k9FUS8ieV4gobmk/Re4mCMFPvgiJgjwmPYm2/miNE+waTDGrVpKcV2aYiZSMpw/Qos6ENxeFLDxEeSzXIYBGgWY1L8ZQnNdzaqO3ayi8Md4m39vOSPMS+o6WMh1nEe9QZhEa87TcfR7qoJiqk3OrFI+zovnbqvg2LKzRvliq+BktuAUInGCMMcaXhwzNeZj0xmNczdZP1zMxQShlv+533qpkl7bQBfyB1DbeIZ8Xa9loWaFD97XBbmI2YmBjomoyv03vYj9QSjI4NwIWESxQ3DUrinQEnLZKRoMP/2ycQ+X67T09XXLlCWW8lqguZ/KBJu801MzKHxxhy9QUhTm74KxqKfLO+Ka8bF4YVPMhDhXyOIZrTWhCdDsBdmDds6YME2zD9V6ss9+1ObbPDnAhg4ii7f4BUrra48Os1sCFkJAXU1ChU4O9CUiAS9SckM4Ha1rirpj4RHE2066Jhee,iv:H/eWpCNe6n53Ntvgmy16MiDlOoABy/kuET3dwqr1uRQ=,tag:7YgkxQZ3pxiWIcXYp2pkvw==,type:str]
ddns:
dynu:
password: ENC[AES256_GCM,data:HvSmMm4IVoeBWxrYj8Yfun7JedU=,iv:l+Y9QRgFEX//c1JIPM9GlTiNi8RmTT62XYVVnhzF0Mk=,tag:2beGCSkMPEF8L3MnllD9SQ==,type:str]
sops:
age:
- recipient: age15l7h4scc9fgprgpfnlfa00s6jz6hecqmuhud3qv38lsjvq8p43lqaqq6n7
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrVXBodXA2MHNjZGpCdTBp
S0ZjQkNpUGpkZGc3aWZmVytxdFdjVUVPK0ZFCi9DQVJKWkFXQzdIYW5zRmd1dHoz
M2I5QXU3MnBRcW5ja29yRGlhM2t3aUEKLS0tIG10MGNZdmNPQmg4aFZCbVZtLzRk
MnozVHhUOTE0SW9SSGxTRnVlQXNQU3MKzzLSIfZ/9R54wcv0e1mYhX6srO08aaaq
M5u6L24X+cb9ACJCyo0/a0Hx/Vj1scln7tA1l2uUcTO56E6GU+0c4w==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEWE15VWNhbW5JdVJrMGlV
a09pNmtMbWttM2V0QXRTc0dpeDhMWFdHZWxJCnNYeWlENTBUb0JQN1MvL0ozMW9v
VEljN21oUmlIdGhGYklrWEc5VW45Y2MKLS0tICtaNTR4ODRMQnV3YUVXSEdVbDAz
VkdwMmhmNkVzZzNPeGZUZEh6YVhqM0EKYOlerQW0E0soPjQLGah/ZquBy30lSWro
WlG0kqbMRYR0cDDmuy8LQW41uiH5ZOP5oRjZuuSr2Z+wd1cpGn0rAg==
-----END AGE ENCRYPTED FILE-----
- recipient: age1j7spzd26l7zryxsrf4r5f3jj7cv5tm7kastp6294fdl8qz975u3sku09hv
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4VkxGeXdpbTliei96cHNi
R3hzQUVPSCtPRW9NOTBPK2I2b29aU1JTZGw4Ckl0NFFKRFZVN0I3UXZHNkhYdlJC
UTZWaDFZc3NyM3VORlFDUXFLdTI5ancKLS0tIDBhYkhUSlNCdk9NQXA2QzEvMVZS
aWpUNzVkUVFhQzEwRXdMaUJOTmdqM1EKf9Ff+Kt1KuLIfwp+W6Bn4ZHViZL1Azsw
ypKEfH8PYqSwPipDzFOK0VOCN4Iti651D9X0oyZe7Q41liqFlvnOOQ==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWaFlMQmdqQVZWTFk1bU9u
aytYaDNGQ1VOeTdtSHE1MTlIbnRXTmxSZWtrClUxRnNTSnF5MnZFMWdJVWF1UnVF
V2NSWDU3aTdHSDNxSTJJNFpHSlV0NUEKLS0tIER5Ly9lcFFLbzVVazErTGF4Z1Jo
eXBuNnVBYjlhVVE4N0tOT0IxQlA4emMKQl/gNsP2H9sf/PVfQwjhK49aWfAd55Zl
VnAMkOYr8JWllQLdl/5v5WZUlpz6726KlmM4ef1WCpPRnrXpTXJJcQ==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-02-22T03:34:23Z"
mac: ENC[AES256_GCM,data:7KZh12b7J/N/HxKhA8IsSxS9Kf5NcdlhYpWJRkMrMhvIXFtIKFo+PCt9b7ajob2WVF4ASs4wv2xxYmxaFTEPHunxhPHyVvt0oGkev63nOXlGu3dpXA28M6tdgkab/ho605WyDwoE4AzlSPKo6aa5NaueRnByjbncR8ow6k/HeMU=,iv:cCCAGmrGbhoYZhSpEFLccbOJsU5gmZivYccGjL5Wv3Y=,tag:65go27f6OP/SDrH+Z87Wfg==,type:str]
lastmodified: "2026-02-24T01:34:36Z"
mac: ENC[AES256_GCM,data:n0IXpEnpu8hzBS1JFLs5khf2LilfObeZzGzJ6U6bzRwzJuEdPj9pBPrLd6+Z9aAIqKP4mAa89q9dNsmgrzfOzPFVNP/aloDFDh+tNbQ/WMk62Fk3ocV9DKGhKIDXaiIcSnzNjarDF0DRomdE9D4W8usW1jJtkl3EwXTKlv1n+eY=,iv:05jEc2SbVL9nMS0STwUNxaj/W0zQ3srGLV9ObiKdbPQ=,tag:KaF4Y4Zqr4PY3iN4ewDdCw==,type:str]
unencrypted_suffix: _unencrypted
version: 3.11.0
Loading