Compare commits

...

8 Commits

Author SHA1 Message Date
59f2639061 ci: skip deploy-rs checks (build step already validates)
Some checks failed
Build and Deploy / deploy (push) Failing after 2m2s
2026-03-30 21:32:15 -04:00
a6c40df359 ci: add git-crypt unlock for server-config build-time secrets
Some checks failed
Build and Deploy / deploy (push) Failing after 15m15s
2026-03-30 21:14:54 -04:00
ed7fda31fe workflow: add SSH access to build step for git+ssh flake inputs
Some checks failed
Build and Deploy / deploy (push) Failing after 4m15s
2026-03-30 21:04:11 -04:00
519eb3a3bb fix: re-encrypt age secrets with SSH pubkey recipient (-R not -r)
Some checks failed
Build and Deploy / deploy (push) Failing after 3m4s
X25519 stanzas from ssh-to-age are incompatible with raw SSH identity.
Use age -R with SSH public key directly to produce ssh-ed25519 stanzas.
Updated AGENTS.md to document the correct process and warn against ssh-to-age.
2026-03-30 19:34:56 -04:00
bef350e5e9 ci: add hosts entry for desktop deploy target 2026-03-30 19:14:30 -04:00
476de03bf4 tests: add NixOS VM test for gitea actions runner 2026-03-30 17:45:17 -04:00
bedc94cbc0 gitea: add actions runner and CI/CD deploy workflow
- enable gitea actions
- add native host runner (nix:host label, capacity 1)
- add gitea-runner system user with persisted state
- add agenix-encrypted CI secrets (deploy key, git-crypt key, runner token)
- authorize CI deploy key for root SSH
- add build-and-deploy workflow triggered on push to main
2026-03-30 17:27:47 -04:00
936efaa21b migrate build-time secrets to agenix runtime
- coturn: switch static-auth-secret to static-auth-secret-file
- matrix: switch registration_token and turn_secret to file-based
- murmur: switch password to environmentFile with agenix
- p2pool: move public wallet address to service-configs.nix
2026-03-30 17:14:47 -04:00
21 changed files with 258 additions and 10 deletions

View File

@@ -0,0 +1,54 @@
name: Build and Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: nix
env:
GIT_SSH_COMMAND: "ssh -i /run/agenix/ci-deploy-key -o StrictHostKeyChecking=no"
steps:
- uses: https://github.com/actions/checkout@v4
with:
fetch-depth: 0
- name: Unlock git-crypt
run: |
git-crypt unlock /run/agenix/git-crypt-key-server-config
- name: Build NixOS configuration
run: |
nix build .#nixosConfigurations.muffin.config.system.build.toplevel -L
- name: Deploy via deploy-rs
run: |
eval $(ssh-agent -s)
ssh-add /run/agenix/ci-deploy-key
nix run github:serokell/deploy-rs -- .#muffin --skip-checks --ssh-opts="-o StrictHostKeyChecking=no"
- name: Health check
run: |
sleep 10
ssh -i /run/agenix/ci-deploy-key -o StrictHostKeyChecking=no root@server-public \
"systemctl is-active gitea && systemctl is-active caddy && systemctl is-active continuwuity && systemctl is-active coturn"
- name: Notify success
if: success()
run: |
curl -sf -X POST \
"https://ntfy.sigkill.computer/deployments" \
-H "Title: [muffin] Deploy succeeded" \
-H "Priority: default" \
-H "Tags: white_check_mark" \
-d "server-config deployed from commit ${GITHUB_SHA::8}"
- name: Notify failure
if: failure()
run: |
curl -sf -X POST \
"https://ntfy.sigkill.computer/deployments" \
-H "Title: [muffin] Deploy FAILED" \
-H "Priority: urgent" \
-H "Tags: rotating_light" \
-d "server-config deploy failed at commit ${GITHUB_SHA::8}"

View File

@@ -99,7 +99,11 @@ Each service file in `services/` follows this structure:
- **git-crypt**: `secrets/` directory and `usb-secrets/usb-secrets-key*` are encrypted (see `.gitattributes`) - **git-crypt**: `secrets/` directory and `usb-secrets/usb-secrets-key*` are encrypted (see `.gitattributes`)
- **agenix**: secrets declared in `modules/age-secrets.nix`, decrypted at runtime to `/run/agenix/` - **agenix**: secrets declared in `modules/age-secrets.nix`, decrypted at runtime to `/run/agenix/`
- **Identity**: USB drive at `/mnt/usb-secrets/usb-secrets-key` - **Identity**: USB drive at `/mnt/usb-secrets/usb-secrets-key`
- **Encrypting new secrets**: The agenix encryption key is in `usb-secrets/usb-secrets-key` (SSH private key, git-crypt encrypted). To create a new secret: derive the age public key with `ssh-keygen -y -f usb-secrets/usb-secrets-key | ssh-to-age`, then encrypt with `age -r <public-key> -o secrets/<name>.age`. - **Encrypting new secrets**: The agenix identity is an SSH private key at `usb-secrets/usb-secrets-key` (git-crypt encrypted). To encrypt a new secret, use the SSH public key directly with `age -R`:
```bash
age -R <(ssh-keygen -y -f usb-secrets/usb-secrets-key) -o secrets/<name>.age /path/to/plaintext
```
- **DO NOT use `ssh-to-age`**. Using `ssh-to-age` to derive a native age public key and then encrypting with `age -r age1...` produces `X25519` recipient stanzas. The SSH private key identity on the server can only decrypt `ssh-ed25519` stanzas. This mismatch causes `age: error: no identity matched any of the recipients` at deploy time. Always use `age -R` with the SSH public key directly.
- Never read or commit plaintext secrets. Never log secret values. - Never read or commit plaintext secrets. Never log secret values.
### Important Patterns ### Important Patterns

View File

@@ -26,6 +26,7 @@
./services/caddy.nix ./services/caddy.nix
./services/immich.nix ./services/immich.nix
./services/gitea.nix ./services/gitea.nix
./services/gitea-actions-runner.nix
./services/minecraft.nix ./services/minecraft.nix
./services/wg.nix ./services/wg.nix
@@ -73,6 +74,9 @@
./services/mollysocket.nix ./services/mollysocket.nix
]; ];
# Hosts entries for CI/CD deploy targets
networking.hosts."192.168.1.223" = [ "desktop" ];
services.kmscon.enable = true; services.kmscon.enable = true;
systemd.targets = { systemd.targets = {
@@ -249,6 +253,14 @@
users.groups.${service_configs.media_group} = { }; users.groups.${service_configs.media_group} = { };
users.users.gitea-runner = {
isSystemUser = true;
group = "gitea-runner";
home = "/var/lib/gitea-runner";
description = "Gitea Actions CI runner";
};
users.groups.gitea-runner = { };
users.users.${username} = { users.users.${username} = {
isNormalUser = true; isNormalUser = true;
extraGroups = [ extraGroups = [
@@ -290,7 +302,8 @@
enable = true; enable = true;
openFirewall = true; openFirewall = true;
welcometext = "meow meow meow meow meow :3 xd"; welcometext = "meow meow meow meow meow :3 xd";
password = builtins.readFile ./secrets/murmur_password; password = "$MURMURD_PASSWORD";
environmentFile = config.age.secrets.murmur-password-env.path;
port = service_configs.ports.public.murmur.port; port = service_configs.ports.public.murmur.port;
}; };

View File

@@ -94,5 +94,70 @@
file = ../secrets/mollysocket-env.age; file = ../secrets/mollysocket-env.age;
mode = "0400"; mode = "0400";
}; };
# Murmur (Mumble) server password
murmur-password-env = {
file = ../secrets/murmur-password-env.age;
mode = "0400";
owner = "murmur";
group = "murmur";
};
# Coturn static auth secret
coturn-auth-secret = {
file = ../secrets/coturn-auth-secret.age;
mode = "0400";
owner = "turnserver";
group = "turnserver";
};
# Matrix (continuwuity) registration token
matrix-reg-token = {
file = ../secrets/matrix-reg-token.age;
mode = "0400";
owner = "continuwuity";
group = "continuwuity";
};
# Matrix (continuwuity) TURN secret — same secret as coturn-auth-secret,
# decrypted separately so continuwuity can read it with its own ownership
matrix-turn-secret = {
file = ../secrets/coturn-auth-secret.age;
mode = "0400";
owner = "continuwuity";
group = "continuwuity";
};
# CI deploy SSH key
ci-deploy-key = {
file = ../secrets/ci-deploy-key.age;
mode = "0400";
owner = "gitea-runner";
group = "gitea-runner";
};
# Git-crypt symmetric key for dotfiles repo
git-crypt-key-dotfiles = {
file = ../secrets/git-crypt-key-dotfiles.age;
mode = "0400";
owner = "gitea-runner";
group = "gitea-runner";
};
# Git-crypt symmetric key for server-config repo
git-crypt-key-server-config = {
file = ../secrets/git-crypt-key-server-config.age;
mode = "0400";
owner = "gitea-runner";
group = "gitea-runner";
};
# Gitea Actions runner registration token
gitea-runner-token = {
file = ../secrets/gitea-runner-token.age;
mode = "0400";
owner = "gitea-runner";
group = "gitea-runner";
};
}; };
} }

View File

@@ -24,6 +24,7 @@
# ZFS cache directory - persisting the directory instead of the file # ZFS cache directory - persisting the directory instead of the file
# avoids "device busy" errors when ZFS atomically updates the cache # avoids "device busy" errors when ZFS atomically updates the cache
"/etc/zfs" "/etc/zfs"
"/var/lib/gitea-runner"
]; ];
files = [ files = [

BIN
secrets/ci-deploy-key.age Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -212,6 +212,7 @@ rec {
p2pool = { p2pool = {
dataDir = services_dir + "/p2pool"; dataDir = services_dir + "/p2pool";
walletAddress = "49b6NT2k7fQHs8JvF7naUvchYwTQmRpoMMXb1KJTg5UcZVmyPJ7n6jgiH8DrvEsMg5GvMjJqPB1c1PTBAYtUTsbeHe5YMBx";
}; };
matrix = { matrix = {

View File

@@ -9,7 +9,7 @@
enable = true; enable = true;
realm = service_configs.https.domain; realm = service_configs.https.domain;
use-auth-secret = true; use-auth-secret = true;
static-auth-secret = lib.strings.trim (builtins.readFile ../secrets/coturn_static_auth_secret); static-auth-secret-file = config.age.secrets.coturn-auth-secret.path;
listening-port = service_configs.ports.public.coturn.port; listening-port = service_configs.ports.public.coturn.port;
tls-listening-port = service_configs.ports.public.coturn_tls.port; tls-listening-port = service_configs.ports.public.coturn_tls.port;
no-cli = true; no-cli = true;

View File

@@ -0,0 +1,46 @@
{
config,
lib,
pkgs,
service_configs,
...
}:
{
services.gitea-actions-runner.instances.muffin = {
enable = true;
name = "muffin";
url = config.services.gitea.settings.server.ROOT_URL;
tokenFile = config.age.secrets.gitea-runner-token.path;
labels = [ "nix:host" ];
hostPackages = with pkgs; [
bash
coreutils
curl
gawk
git
git-crypt
gnugrep
gnused
jq
nix
nodejs
openssh
];
settings = {
runner = {
capacity = 1;
timeout = "3h";
};
};
};
# Override DynamicUser to use our static gitea-runner user
systemd.services."gitea-runner-muffin" = {
serviceConfig = {
DynamicUser = lib.mkForce false;
User = "gitea-runner";
Group = "gitea-runner";
};
environment.GIT_SSH_COMMAND = "ssh -i /run/agenix/ci-deploy-key -o StrictHostKeyChecking=no";
};
}

View File

@@ -37,6 +37,7 @@
}; };
# only I shall use gitea # only I shall use gitea
service.DISABLE_REGISTRATION = true; service.DISABLE_REGISTRATION = true;
actions.ENABLED = true;
}; };
}; };

View File

@@ -21,7 +21,7 @@
port = [ service_configs.ports.private.matrix.port ]; port = [ service_configs.ports.private.matrix.port ];
server_name = service_configs.https.domain; server_name = service_configs.https.domain;
allow_registration = true; allow_registration = true;
registration_token = lib.strings.trim (builtins.readFile ../secrets/matrix_reg_token); registration_token_file = config.age.secrets.matrix-reg-token.path;
new_user_displayname_suffix = ""; new_user_displayname_suffix = "";
@@ -37,7 +37,7 @@
]; ];
# TURN server config (coturn) # TURN server config (coturn)
turn_secret = config.services.coturn.static-auth-secret; turn_secret_file = config.age.secrets.matrix-turn-secret.path;
turn_uris = [ turn_uris = [
"turn:${service_configs.https.domain}?transport=udp" "turn:${service_configs.https.domain}?transport=udp"
"turn:${service_configs.https.domain}?transport=tcp" "turn:${service_configs.https.domain}?transport=tcp"

View File

@@ -4,9 +4,6 @@
lib, lib,
... ...
}: }:
let
walletAddress = lib.strings.trim (builtins.readFile ../secrets/xmrig-wallet);
in
{ {
imports = [ imports = [
(lib.serviceMountWithZpool "p2pool" service_configs.zpool_ssds [ (lib.serviceMountWithZpool "p2pool" service_configs.zpool_ssds [
@@ -20,7 +17,7 @@ in
services.p2pool = { services.p2pool = {
enable = true; enable = true;
dataDir = service_configs.p2pool.dataDir; dataDir = service_configs.p2pool.dataDir;
walletAddress = walletAddress; walletAddress = service_configs.p2pool.walletAddress;
sidechain = "nano"; sidechain = "nano";
host = "127.0.0.1"; host = "127.0.0.1";
rpcPort = service_configs.ports.public.monero_rpc.port; rpcPort = service_configs.ports.public.monero_rpc.port;

View File

@@ -31,5 +31,8 @@
# used for deploying configs to server # used for deploying configs to server
users.users.root.openssh.authorizedKeys.keys = users.users.root.openssh.authorizedKeys.keys =
config.users.users.${username}.openssh.authorizedKeys.keys; config.users.users.${username}.openssh.authorizedKeys.keys
++ [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC5ZYN6idL/w/mUIfPOH1i+Q/SQXuzAMQUEuWpipx1Pc ci-deploy@muffin"
];
} }

60
tests/gitea-runner.nix Normal file
View File

@@ -0,0 +1,60 @@
{
config,
lib,
pkgs,
...
}:
pkgs.testers.runNixOSTest {
name = "gitea-runner";
nodes.machine =
{ pkgs, ... }:
{
services.gitea = {
enable = true;
database.type = "sqlite3";
settings = {
server = {
HTTP_PORT = 3000;
ROOT_URL = "http://localhost:3000";
DOMAIN = "localhost";
};
actions.ENABLED = true;
service.DISABLE_REGISTRATION = true;
};
};
specialisation.runner = {
inheritParentConfig = true;
configuration.services.gitea-actions-runner.instances.test = {
enable = true;
name = "ci";
url = "http://localhost:3000";
labels = [ "native:host" ];
tokenFile = "/var/lib/gitea/runner_token";
};
};
};
testScript = ''
start_all()
machine.wait_for_unit("gitea.service")
machine.wait_for_open_port(3000)
# Generate runner token
machine.succeed(
"su -l gitea -s /bin/sh -c '${pkgs.gitea}/bin/gitea actions generate-runner-token --work-path /var/lib/gitea' | tail -1 | sed 's/^/TOKEN=/' > /var/lib/gitea/runner_token"
)
# Switch to runner specialisation
machine.succeed(
"/run/current-system/specialisation/runner/bin/switch-to-configuration test"
)
# Start the runner (specialisation switch doesn't auto-start new services)
machine.succeed("systemctl start gitea-runner-test.service")
machine.wait_for_unit("gitea-runner-test.service")
machine.succeed("sleep 5")
machine.succeed("test -f /var/lib/gitea-runner/test/.runner")
'';
}

View File

@@ -27,4 +27,7 @@ in
# torrent audit test # torrent audit test
torrentAuditTest = handleTest ./torrent-audit.nix; torrentAuditTest = handleTest ./torrent-audit.nix;
# gitea runner test
giteaRunnerTest = handleTest ./gitea-runner.nix;
} }