sops-nix bootstrap + Forgejo at git.tyrolize.ch

sops:
- devShell provides ssh-to-age and sets SOPS_AGE_KEY_FILE to
  $REPO/.sops-age-key.txt (gitignored, generated locally).
- .sops.yaml lists laptop + watcher recipients. The watcher recipient is
  derived from /etc/ssh/ssh_host_ed25519_key.pub via ssh-to-age, so the
  watcher decrypts using its SSH host key as the age identity at boot.
- hosts/watcher/secrets.yaml holds an `example` placeholder; sops-install-
  secrets surfaces it at /run/secrets/example (root-only).

Forgejo:
- modules/forgejo.nix enables services.forgejo (sqlite + daily tar.gz
  dump), built-in SSH server on :2222, HTTP on 127.0.0.1:3000.
- modules/website.nix adds the git.tyrolize.ch vhost reverse-proxying to
  localhost. Caddy gets a Let's Encrypt cert automatically.
- terraform/infomaniak/watcher.tf opens :2222 v4+v6 in the security group.
- Admin user `tyro` (role admin) created out-of-band via the gitea CLI.

Both services live on the watcher.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
tyrolize 2026-06-16 23:59:55 +02:00
parent 59d742f4ba
commit 1f9c2669a2
8 changed files with 145 additions and 7 deletions

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
# Secrets — never commit # Secrets — never commit
.env .env
.sops-age-key.txt
clouds.yaml clouds.yaml
*.tfvars *.tfvars
*.pem *.pem

View file

@ -1,14 +1,24 @@
# sops-nix encryption rules. # sops-nix encryption rules.
# #
# To get started: # Recipients:
# nix-shell -p age --run 'mkdir -p ~/.config/sops/age && age-keygen -o ~/.config/sops/age/keys.txt' # - laptop: the age key in $REPO/.sops-age-key.txt (gitignored).
# Then copy the public key (the line starting with "# public key:") into the # To recover: keep a copy of that file in your password manager.
# placeholder below. # - watcher: derived from the watcher's SSH host key
# (/etc/ssh/ssh_host_ed25519_key). If the watcher is rebuilt without
# restoring that host key, regenerate the recipient with:
# ssh tyro@<watcher> cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age
# and update this file accordingly.
#
# To edit an encrypted file: `sops hosts/watcher/secrets.yaml`
# To re-encrypt all files for new/changed keys: `sops updatekeys hosts/watcher/secrets.yaml`
keys: keys:
- &laptop age1REPLACE_ME_WITH_YOUR_LAPTOP_PUBLIC_KEY - &laptop age12hw3c0qfhl2ezk4aawgax3qu3a6gt5vm300xqtzwsl5l7mj903pq4kw8pf
- &watcher age1ck8zheqpudkc6zsgfujyf287zte3q07fa05wkqwfv3raz7snsf9sk7s8zf
creation_rules: creation_rules:
- path_regex: hosts/watcher/secrets\.yaml$ - path_regex: hosts/watcher/secrets\.yaml$
key_groups: key_groups:
- age: [*laptop] - age:
- *laptop
- *watcher

View file

@ -23,13 +23,15 @@
opentofu # `tofu` — drop-in for terraform, OSS license opentofu # `tofu` — drop-in for terraform, OSS license
jq jq
age age
sops # for sops-nix secrets sops # encrypted secrets, paired with sops-nix
ssh-to-age # convert an SSH host key to an age recipient
]; ];
shellHook = '' shellHook = ''
if [ -f "$PWD/terraform/infomaniak/.env" ]; then if [ -f "$PWD/terraform/infomaniak/.env" ]; then
set -a; . "$PWD/terraform/infomaniak/.env"; set +a set -a; . "$PWD/terraform/infomaniak/.env"; set +a
echo "loaded terraform/infomaniak/.env" echo "loaded terraform/infomaniak/.env"
fi fi
export SOPS_AGE_KEY_FILE="$PWD/.sops-age-key.txt"
''; '';
}; };

View file

@ -1,8 +1,19 @@
{ config, lib, pkgs, ... }: { { config, lib, pkgs, ... }: {
imports = [ imports = [
../../modules/website.nix ../../modules/website.nix
../../modules/forgejo.nix
]; ];
# sops-nix: encrypted secrets in ./secrets.yaml, decrypted at boot using
# the watcher's SSH host key as the age identity. Plaintext lands in
# /run/secrets/<name>, readable only by root by default. Edit with
# `sops hosts/watcher/secrets.yaml` from inside `nix develop`.
sops = {
defaultSopsFile = ./secrets.yaml;
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
secrets.example = { }; # placeholder to confirm the pipeline works
};
# Boot, disk layout, and cloud-init are provided by: # Boot, disk layout, and cloud-init are provided by:
# - openstack-config.nix (for nixos-rebuild on the live box), or # - openstack-config.nix (for nixos-rebuild on the live box), or
# - openstack-image.nix (when building the QCOW2 image) # - openstack-image.nix (when building the QCOW2 image)

View file

@ -0,0 +1,27 @@
#ENC[AES256_GCM,data:Di03efwg2Ta3FDvLeHf9axkamX/MZ6IUDRL+aAEXol4J+RNZh1zF0Q7OydzL0DNp/GO5+mbeV412C03QqcNw/LyoOvr0tQax5gvb1qOXQ3ZoBE8=,iv:Ekyjlc2DQhF2g4wBq0mism7xgA4ijIu0tR5XbqfH8Fs=,tag:PC5mxMSQA4oEEKiibzLO6A==,type:comment]
#ENC[AES256_GCM,data:hUylzsdMw2FqS3dZgEJID6t0K1faXXXqpuaaZS11ZoLPsQVmzeBqOr4m2Q==,iv:IiW5X67mtkGenGGLkQxqMnK4IwIsOcptuTnGUiAdmUg=,tag:ufV8dmOL3mP76ssqL53r/g==,type:comment]
example: ENC[AES256_GCM,data:ZuUX5vadaSXv9QgPdhOa,iv:6EykcZ/7pE8aHGfw3P0V4c3iptCVFX9N7qPGaQXtpsk=,tag:aaVv2FilGUP++mVlJZGRAA==,type:str]
sops:
age:
- recipient: age12hw3c0qfhl2ezk4aawgax3qu3a6gt5vm300xqtzwsl5l7mj903pq4kw8pf
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoMUFFSm1hK1k2R2NLMjFI
cXZtamRUaWpJZ2VwUWxxZCtxNWpIenkyYlVVCmpQSkJLTDVtRkxpVmhWbFhZZGtN
UGh1VkdwTThCZjhTc0tOdXQyK0VwVnMKLS0tIERyV1V1TFdZS2grMmdGM01mTnRG
eWM0SUZjWVB3UEQyWlkyZkpPVTNLVzgKPPDYWvMhlW1AutxX4In4RKD6ThQNYWd6
tcri8OW3WXeVsaZu3oG0Lk+dic1W+Ii/FDY9huXjTzg65e2JViEF2A==
-----END AGE ENCRYPTED FILE-----
- recipient: age1ck8zheqpudkc6zsgfujyf287zte3q07fa05wkqwfv3raz7snsf9sk7s8zf
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaRjlCcWh0UXhqaHFoTTUv
cTJFdmEwSWpqcDgvR1RSNVM4M0xnSUE1eHc4ClFTN2MxdDV4VW0wc1B0dE1IN1Bu
MVl4Um9xd3hYTEJGTHFkVVdwdEJuUDgKLS0tIE5PNmFxR1N4Z293ckRaZ3cvVm12
MFlOWWtQYUZjcGhNOTAwWWwzWFRqZFUK7kxjCXAreCIgqhZiKmdwVQg5hGm+b0/J
0Zw7zf1OWwV5o3qI5V6MLEUT5QYVy6QJQ56zFvi/fCmjr+ET3QC57g==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-06-16T21:44:28Z"
mac: ENC[AES256_GCM,data:HhFyw1zNlMfvSshC9xX6YIZ95TUMZnG2ug7Gt9U5Kny5hZg5S5NsGM8/jlmaYejDESyxBsHFW6i+9hzFOeTnGdL6ou3LVJslJGGjS0x9PU13VaqaGAMKlDNWIz5XWNFOt6tue8i1JQE8h2iDHHlN2SDgYEGzVyPMl4hSxc+BoXI=,iv:9xZbfJS6m9xnOHwAvwLP6OLqxyNmzKEh3l/zawN4Jks=,tag:fS/b3x3CqlPAq3eT6bBjdA==,type:str]
unencrypted_suffix: _unencrypted
version: 3.11.0

59
modules/forgejo.nix Normal file
View file

@ -0,0 +1,59 @@
{ config, lib, pkgs, ... }: {
# Forgejo: self-hosted git, accessed at https://git.tyrolize.ch (Caddy
# reverse-proxies to 127.0.0.1:3000 — vhost lives in modules/website.nix)
# and ssh://git@git.tyrolize.ch:2222 for repo push/pull.
services.forgejo = {
enable = true;
# Single-user scale — sqlite is plenty and simplifies backups.
database.type = "sqlite3";
# Daily compressed dump of repos + config + DB into /var/lib/forgejo/dump.
# Restic will pick it up later.
dump = {
enable = true;
type = "tar.gz";
};
lfs.enable = true;
settings = {
server = {
DOMAIN = "git.tyrolize.ch";
ROOT_URL = "https://git.tyrolize.ch/";
# Listen on loopback only; Caddy provides public TLS.
HTTP_ADDR = "127.0.0.1";
HTTP_PORT = 3000;
# Built-in SSH server — separate from system sshd on :22.
START_SSH_SERVER = true;
SSH_DOMAIN = "git.tyrolize.ch";
SSH_LISTEN_HOST = "0.0.0.0";
SSH_PORT = 2222;
SSH_LISTEN_PORT = 2222;
LANDING_PAGE = "explore";
};
service = {
DISABLE_REGISTRATION = true;
# Allow admin to create users via the CLI / UI (defaults are fine).
REQUIRE_SIGNIN_VIEW = false;
};
session.COOKIE_SECURE = true;
log.LEVEL = "Info";
# Allow embedding of the Forgejo UI from itself only (default), and
# tighten a couple of small things.
"ui.meta" = {
AUTHOR = "tyrolize";
DESCRIPTION = "tyrolize's git";
};
# Disable the install wizard — NixOS provides the config.
security.INSTALL_LOCK = true;
};
};
networking.firewall.allowedTCPPorts = [ 2222 ];
}

View file

@ -22,5 +22,12 @@
redir https://tyrolize.ch{uri} permanent redir https://tyrolize.ch{uri} permanent
''; '';
}; };
virtualHosts."git.tyrolize.ch" = {
extraConfig = ''
reverse_proxy 127.0.0.1:3000
encode gzip zstd
'';
};
}; };
} }

View file

@ -60,6 +60,27 @@ resource "openstack_networking_secgroup_rule_v2" "https" {
security_group_id = openstack_networking_secgroup_v2.watcher.id security_group_id = openstack_networking_secgroup_v2.watcher.id
} }
# Forgejo's built-in SSH server (separate from system sshd on :22).
resource "openstack_networking_secgroup_rule_v2" "forgejo_ssh_v4" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 2222
port_range_max = 2222
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.watcher.id
}
resource "openstack_networking_secgroup_rule_v2" "forgejo_ssh_v6" {
direction = "ingress"
ethertype = "IPv6"
protocol = "tcp"
port_range_min = 2222
port_range_max = 2222
remote_ip_prefix = "::/0"
security_group_id = openstack_networking_secgroup_v2.watcher.id
}
resource "openstack_compute_instance_v2" "watcher" { resource "openstack_compute_instance_v2" "watcher" {
name = "watcher" name = "watcher"
flavor_name = var.flavor_name flavor_name = var.flavor_name