diff --git a/.gitignore b/.gitignore index 62efe91..2843ad7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Secrets — never commit .env +.sops-age-key.txt clouds.yaml *.tfvars *.pem diff --git a/.sops.yaml b/.sops.yaml index e18a848..d1cf2e2 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -1,14 +1,24 @@ # sops-nix encryption rules. # -# To get started: -# nix-shell -p age --run 'mkdir -p ~/.config/sops/age && age-keygen -o ~/.config/sops/age/keys.txt' -# Then copy the public key (the line starting with "# public key:") into the -# placeholder below. +# Recipients: +# - laptop: the age key in $REPO/.sops-age-key.txt (gitignored). +# To recover: keep a copy of that file in your password manager. +# - 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@ 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: - - &laptop age1REPLACE_ME_WITH_YOUR_LAPTOP_PUBLIC_KEY + - &laptop age12hw3c0qfhl2ezk4aawgax3qu3a6gt5vm300xqtzwsl5l7mj903pq4kw8pf + - &watcher age1ck8zheqpudkc6zsgfujyf287zte3q07fa05wkqwfv3raz7snsf9sk7s8zf creation_rules: - path_regex: hosts/watcher/secrets\.yaml$ key_groups: - - age: [*laptop] + - age: + - *laptop + - *watcher diff --git a/flake.nix b/flake.nix index 048aaf9..6b1f248 100644 --- a/flake.nix +++ b/flake.nix @@ -23,13 +23,15 @@ opentofu # `tofu` — drop-in for terraform, OSS license jq 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 = '' if [ -f "$PWD/terraform/infomaniak/.env" ]; then set -a; . "$PWD/terraform/infomaniak/.env"; set +a echo "loaded terraform/infomaniak/.env" fi + export SOPS_AGE_KEY_FILE="$PWD/.sops-age-key.txt" ''; }; diff --git a/hosts/watcher/default.nix b/hosts/watcher/default.nix index a3713ea..30fa77b 100644 --- a/hosts/watcher/default.nix +++ b/hosts/watcher/default.nix @@ -1,8 +1,19 @@ { config, lib, pkgs, ... }: { imports = [ ../../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/, 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: # - openstack-config.nix (for nixos-rebuild on the live box), or # - openstack-image.nix (when building the QCOW2 image) diff --git a/hosts/watcher/secrets.yaml b/hosts/watcher/secrets.yaml new file mode 100644 index 0000000..7c4d72f --- /dev/null +++ b/hosts/watcher/secrets.yaml @@ -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 diff --git a/modules/forgejo.nix b/modules/forgejo.nix new file mode 100644 index 0000000..540af49 --- /dev/null +++ b/modules/forgejo.nix @@ -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 ]; +} diff --git a/modules/website.nix b/modules/website.nix index 3f3f60f..5ee5636 100644 --- a/modules/website.nix +++ b/modules/website.nix @@ -22,5 +22,12 @@ redir https://tyrolize.ch{uri} permanent ''; }; + + virtualHosts."git.tyrolize.ch" = { + extraConfig = '' + reverse_proxy 127.0.0.1:3000 + encode gzip zstd + ''; + }; }; } diff --git a/terraform/infomaniak/watcher.tf b/terraform/infomaniak/watcher.tf index ca7ecd0..74614e8 100644 --- a/terraform/infomaniak/watcher.tf +++ b/terraform/infomaniak/watcher.tf @@ -60,6 +60,27 @@ resource "openstack_networking_secgroup_rule_v2" "https" { 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" { name = "watcher" flavor_name = var.flavor_name