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:
parent
59d742f4ba
commit
1f9c2669a2
8 changed files with 145 additions and 7 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,5 +1,6 @@
|
||||||
# Secrets — never commit
|
# Secrets — never commit
|
||||||
.env
|
.env
|
||||||
|
.sops-age-key.txt
|
||||||
clouds.yaml
|
clouds.yaml
|
||||||
*.tfvars
|
*.tfvars
|
||||||
*.pem
|
*.pem
|
||||||
|
|
|
||||||
22
.sops.yaml
22
.sops.yaml
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
27
hosts/watcher/secrets.yaml
Normal file
27
hosts/watcher/secrets.yaml
Normal 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
59
modules/forgejo.nix
Normal 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 ];
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue