Feature/building portal vm on vultr #59
@@ -1,6 +1,9 @@
|
||||
# Generate AGE keys from SSH keys with:
|
||||
# ssh-keygen -A
|
||||
# nix-shell -p ssh-to-age --run 'cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age'
|
||||
# update keys after addin/removing a key
|
||||
# nix-shell -p sops --run "sops updatekeys users/secrets.yaml" users/secrets.yaml
|
||||
|
||||
keys:
|
||||
- &admin_richie age1u8zj599elqqvcmhxn8zuwrufsz8w8w366d3ayrljjejljt2q45kq8mxw9c # cspell:disable-line
|
||||
|
||||
@@ -8,6 +11,7 @@ keys:
|
||||
- &system_brain age1jhf7vm0005j60mjq63696frrmjhpy8kpc2d66mw044lqap5mjv4snmwvwm # cspell:disable-line
|
||||
- &system_jeeves age13lmqgc3jvkyah5e3vcwmj4s5wsc2akctcga0lpc0x8v8du3fxprqp4ldkv # cspell:disable-line
|
||||
- &system_rhapsody age1ufnewppysaq2wwcl4ugngjz8pfzc5a35yg7luq0qmuqvctajcycs5lf6k4 # cspell:disable-line
|
||||
- &system_portal_1 age1vyav6kxtvt3z4vtnkkjj38eu8hlts5m7ygyckhskvalg2gpjk52su53d0a # cspell:disable-line
|
||||
|
||||
creation_rules:
|
||||
- path_regex: users/secrets\.yaml$
|
||||
@@ -18,3 +22,4 @@ creation_rules:
|
||||
- *system_brain
|
||||
- *system_jeeves
|
||||
- *system_rhapsody
|
||||
- *system_portal_1
|
||||
|
||||
@@ -17,16 +17,11 @@
|
||||
./nix.nix
|
||||
./programs.nix
|
||||
./ssh.nix
|
||||
./snapshot_manager.nix
|
||||
];
|
||||
|
||||
boot = {
|
||||
tmp.useTmpfs = true;
|
||||
tmp.useTmpfs = lib.mkDefault true;
|
||||
kernelPackages = lib.mkDefault pkgs.linuxPackages_6_12;
|
||||
zfs = {
|
||||
package = lib.mkDefault pkgs.zfs_2_4;
|
||||
forceImportRoot = lib.mkDefault false;
|
||||
};
|
||||
};
|
||||
|
||||
hardware.enableRedistributableFirmware = true;
|
||||
@@ -50,16 +45,6 @@
|
||||
|
||||
# firmware update
|
||||
fwupd.enable = true;
|
||||
|
||||
snapshot_manager = {
|
||||
enable = lib.mkDefault true;
|
||||
PYTHONPATH = "${inputs.self}/";
|
||||
};
|
||||
|
||||
zfs = {
|
||||
trim.enable = lib.mkDefault true;
|
||||
autoScrub.enable = lib.mkDefault true;
|
||||
};
|
||||
};
|
||||
|
||||
powerManagement.powertop.enable = lib.mkDefault true;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
config,
|
||||
inputs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
nix.settings = {
|
||||
trusted-substituters = [ "http://jeeves:5000" ];
|
||||
substituters = [ "http://jeeves:5000/?priority=1&want-mass-query=true" ];
|
||||
trusted-public-keys = [ "cache.tmmworkshop.com:jHffkpgbmEdstQPoihJPYW9TQe6jnQbWR2LqkNGV3iA=" ];
|
||||
};
|
||||
|
||||
services.tailscale = {
|
||||
enable = true;
|
||||
openFirewall = true;
|
||||
authKeyFile = config.sops.secrets.tailscale_auth_key.path;
|
||||
|
||||
# OAuth client secrets create ephemeral nodes by default. NixOS machines
|
||||
# are persistent and should enroll without interactive device approval.
|
||||
authKeyParameters = {
|
||||
ephemeral = false;
|
||||
preauthorized = true;
|
||||
};
|
||||
|
||||
extraUpFlags = [ "--advertise-tags=tag:nixos" ];
|
||||
};
|
||||
|
||||
sops = {
|
||||
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
|
||||
|
||||
secrets.tailscale_auth_key = {
|
||||
sopsFile = "${inputs.self}/users/secrets.yaml";
|
||||
owner = "root";
|
||||
mode = "0400";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
inputs,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
imports = [ ./snapshot.nix ];
|
||||
|
||||
boot.zfs = {
|
||||
package = lib.mkDefault pkgs.zfs_2_4;
|
||||
forceImportRoot = lib.mkDefault false;
|
||||
};
|
||||
|
||||
services = {
|
||||
snapshot_manager = {
|
||||
enable = lib.mkDefault true;
|
||||
PYTHONPATH = "${inputs.self}/";
|
||||
};
|
||||
|
||||
zfs = {
|
||||
trim.enable = lib.mkDefault true;
|
||||
autoScrub.enable = lib.mkDefault true;
|
||||
};
|
||||
};
|
||||
}
|
||||
Generated
+21
@@ -1,5 +1,25 @@
|
||||
{
|
||||
"nodes": {
|
||||
"disko": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1781152676,
|
||||
"narHash": "sha256-RxWs5ND31KzTG7wvMM+PMfUjyNpmIEr999lqNARaM5o=",
|
||||
"owner": "nix-community",
|
||||
"repo": "disko",
|
||||
"rev": "ff8702b4de27f72b4c78573dfb89ec74e36abdf1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "disko",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"firefox-addons": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
@@ -124,6 +144,7 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"disko": "disko",
|
||||
"firefox-addons": "firefox-addons",
|
||||
"home-manager": "home-manager",
|
||||
"nixos-hardware": "nixos-hardware",
|
||||
|
||||
@@ -35,6 +35,11 @@
|
||||
url = "github:Mic92/sops-nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
disko = {
|
||||
url = "github:nix-community/disko";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs =
|
||||
|
||||
@@ -6,15 +6,17 @@
|
||||
"${inputs.self}/users/steve"
|
||||
"${inputs.self}/common/global"
|
||||
"${inputs.self}/common/optional/docker.nix"
|
||||
"${inputs.self}/common/optional/scanner.nix"
|
||||
"${inputs.self}/common/optional/monitoring-agent.nix"
|
||||
"${inputs.self}/common/optional/nvidia.nix"
|
||||
"${inputs.self}/common/optional/scanner.nix"
|
||||
"${inputs.self}/common/optional/steam.nix"
|
||||
"${inputs.self}/common/optional/syncthing_base.nix"
|
||||
"${inputs.self}/common/optional/systemd-boot.nix"
|
||||
"${inputs.self}/common/optional/tailscale.nix"
|
||||
"${inputs.self}/common/optional/update.nix"
|
||||
"${inputs.self}/common/optional/yubikey.nix"
|
||||
"${inputs.self}/common/optional/zerotier.nix"
|
||||
"${inputs.self}/common/optional/nvidia.nix"
|
||||
"${inputs.self}/common/optional/zfs"
|
||||
./hardware.nix
|
||||
./syncthing.nix
|
||||
./llms.nix
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
"${inputs.self}/common/optional/ssh_decrypt.nix"
|
||||
"${inputs.self}/common/optional/syncthing_base.nix"
|
||||
"${inputs.self}/common/optional/systemd-boot.nix"
|
||||
"${inputs.self}/common/optional/tailscale.nix"
|
||||
"${inputs.self}/common/optional/update.nix"
|
||||
"${inputs.self}/common/optional/zerotier.nix"
|
||||
"${inputs.self}/common/optional/zfs"
|
||||
./docker
|
||||
./hardware.nix
|
||||
./programs.nix
|
||||
|
||||
@@ -13,12 +13,13 @@ in
|
||||
"${inputs.self}/common/optional/monitoring-agent.nix"
|
||||
"${inputs.self}/common/optional/ssh_decrypt.nix"
|
||||
"${inputs.self}/common/optional/syncthing_base.nix"
|
||||
"${inputs.self}/common/optional/tailscale.nix"
|
||||
"${inputs.self}/common/optional/update.nix"
|
||||
"${inputs.self}/common/optional/zerotier.nix"
|
||||
"${inputs.self}/common/optional/zfs"
|
||||
./monitoring
|
||||
./docker
|
||||
./services
|
||||
./web_services
|
||||
./hardware.nix
|
||||
./networking.nix
|
||||
./programs.nix
|
||||
|
||||
@@ -17,8 +17,17 @@
|
||||
allowedTCPPorts = [ ];
|
||||
allowedUDPPorts = [ ];
|
||||
};
|
||||
interfaces.tailscale0.allowedTCPPorts = [
|
||||
5000 # Nix binary cache
|
||||
8000 # Audiobookshelf
|
||||
8001 # Norn Sight
|
||||
8002 # Gems
|
||||
8091 # Share
|
||||
8096 # Jellyfin HTTP
|
||||
];
|
||||
allowedTCPPorts = [
|
||||
8070
|
||||
6443 # Gitea HTTP
|
||||
8070 # ebook searcher
|
||||
];
|
||||
};
|
||||
useNetworkd = true;
|
||||
|
||||
@@ -6,6 +6,7 @@ in
|
||||
services.audiobookshelf = {
|
||||
enable = true;
|
||||
port = 8000;
|
||||
host = "0.0.0.0";
|
||||
};
|
||||
systemd.services.audiobookshelf.serviceConfig.WorkingDirectory =
|
||||
lib.mkForce "${vars.docker_configs}/audiobookshelf";
|
||||
|
||||
@@ -36,7 +36,7 @@ in
|
||||
Type = "simple";
|
||||
User = "gems";
|
||||
Group = "gems";
|
||||
ExecStart = "${pkgs.my_python}/bin/python -m python.gems.main --host 127.0.0.1 --port 8002";
|
||||
ExecStart = "${pkgs.my_python}/bin/python -m python.gems.main --host 0.0.0.0 --port 8002";
|
||||
Restart = "on-failure";
|
||||
RestartSec = "5s";
|
||||
StandardOutput = "journal";
|
||||
|
||||
@@ -3,7 +3,6 @@ let
|
||||
in
|
||||
{
|
||||
networking.firewall.allowedTCPPorts = [
|
||||
6443
|
||||
2223
|
||||
];
|
||||
|
||||
@@ -27,10 +26,14 @@ in
|
||||
};
|
||||
service.DISABLE_REGISTRATION = true;
|
||||
server = {
|
||||
DOMAIN = "tmmworkshop.com";
|
||||
DOMAIN = "gitea.tmmworkshop.com";
|
||||
ROOT_URL = "https://gitea.tmmworkshop.com/";
|
||||
HTTP_PORT = 6443;
|
||||
BUILTIN_SSH_SERVER_USER = "gitea";
|
||||
SSH_USER = "gitea";
|
||||
SSH_DOMAIN = "gitea.tmmworkshop.com";
|
||||
SSH_PORT = 2223;
|
||||
SSH_LISTEN_HOST = "0.0.0.0";
|
||||
SSH_LISTEN_PORT = 2223;
|
||||
START_SSH_SERVER = true;
|
||||
PUBLIC_URL_DETECTION = "auto";
|
||||
|
||||
@@ -4,7 +4,7 @@ in
|
||||
{
|
||||
services.jellyfin = {
|
||||
enable = true;
|
||||
openFirewall = true;
|
||||
openFirewall = false;
|
||||
dataDir = "${vars.services}/jellyfin";
|
||||
cacheDir = "${vars.services}/jellyfin/cache";
|
||||
};
|
||||
|
||||
@@ -7,6 +7,6 @@ in
|
||||
package = pkgs.nix-serve-ng;
|
||||
enable = true;
|
||||
secretKeyFile = "${vars.secrets}/services/nix-cache/cache-priv-key.pem";
|
||||
openFirewall = true;
|
||||
openFirewall = false;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
zpool = ["root_pool", "storage", "media"]
|
||||
services = [
|
||||
"audiobookshelf",
|
||||
"haproxy",
|
||||
"docker",
|
||||
"jellyfin",
|
||||
]
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
let
|
||||
domains = [
|
||||
"audiobookshelf"
|
||||
"cache"
|
||||
"gitea"
|
||||
"gems"
|
||||
"jellyfin"
|
||||
"share"
|
||||
];
|
||||
extraDomains = [ "www.norn-sight.com" ];
|
||||
|
||||
makeCert = name: {
|
||||
name = "${name}.tmmworkshop.com";
|
||||
value = {
|
||||
webroot = "/var/lib/acme/.challenges";
|
||||
group = "acme";
|
||||
reloadServices = [ "haproxy.service" ];
|
||||
};
|
||||
};
|
||||
|
||||
makeExtraCert = name: {
|
||||
inherit name;
|
||||
value = {
|
||||
webroot = "/var/lib/acme/.challenges";
|
||||
group = "acme";
|
||||
reloadServices = [ "haproxy.service" ];
|
||||
};
|
||||
};
|
||||
|
||||
acmeServices =
|
||||
map (domain: "acme-${domain}.tmmworkshop.com.service") domains
|
||||
++ map (domain: "acme-${domain}.service") extraDomains;
|
||||
in
|
||||
{
|
||||
users.users.haproxy.extraGroups = [ "acme" ];
|
||||
|
||||
security.acme = {
|
||||
acceptTerms = true;
|
||||
defaults.email = "Richie@tmmworkshop.com";
|
||||
certs = builtins.listToAttrs ((map makeCert domains) ++ (map makeExtraCert extraDomains));
|
||||
};
|
||||
|
||||
# Minimal nginx to serve ACME HTTP-01 challenge files for HAProxy
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts."acme-challenge" = {
|
||||
listen = [
|
||||
{
|
||||
addr = "127.0.0.1";
|
||||
port = 8402;
|
||||
}
|
||||
];
|
||||
locations."/.well-known/acme-challenge/" = {
|
||||
root = "/var/lib/acme/.challenges";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Ensure the challenge directory exists with correct permissions
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /var/lib/acme/.challenges 0750 acme acme - -"
|
||||
"d /var/lib/acme/.challenges/.well-known 0750 acme acme - -"
|
||||
"d /var/lib/acme/.challenges/.well-known/acme-challenge 0750 acme acme - -"
|
||||
];
|
||||
|
||||
users.users.nginx.extraGroups = [ "acme" ];
|
||||
|
||||
# HAProxy needs certs to exist before it can bind :443.
|
||||
# NixOS's acme module generates self-signed placeholders on first boot
|
||||
# via acme-<domain>.service — just make HAProxy wait for them.
|
||||
systemd.services.haproxy = {
|
||||
after = acmeServices;
|
||||
wants = acmeServices;
|
||||
};
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{ lib, ... }:
|
||||
{
|
||||
imports =
|
||||
let
|
||||
files = builtins.attrNames (builtins.readDir ./.);
|
||||
nixFiles = builtins.filter (name: lib.hasSuffix ".nix" name && name != "default.nix") files;
|
||||
in
|
||||
map (file: ./. + "/${file}") nixFiles;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
networking.firewall.allowedTCPPorts = [
|
||||
80
|
||||
443
|
||||
];
|
||||
|
||||
# Global robots.txt served by HAProxy for every vhost (see haproxy.cfg).
|
||||
environment.etc."haproxy/robots.txt".source = ./robots.txt;
|
||||
|
||||
services.haproxy = {
|
||||
enable = true;
|
||||
config = builtins.readFile ./haproxy.cfg;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
# portal_1
|
||||
|
||||
Minimal NixOS target for a Vultr VM, installed with nixos-anywhere. The Nix
|
||||
flake target is `portal_1`; the machine hostname is `portal-1` because DNS
|
||||
hostnames cannot contain underscores.
|
||||
|
||||
## Before deploying
|
||||
|
||||
1. Confirm the VM's system disk is `/dev/vda`. If it is not, update both
|
||||
references in `disk-config.nix`.
|
||||
2. Confirm the SSH public key in `default.nix` is the key that should have
|
||||
administrator access.
|
||||
3. Boot the VM into a NixOS installer or another nixos-anywhere-compatible
|
||||
Linux rescue environment with root SSH access. Keep this environment
|
||||
running while completing the SOPS bootstrap below.
|
||||
|
||||
## Bootstrap SOPS
|
||||
|
||||
Use the rescue environment's SSH host key as the permanent portal identity.
|
||||
Replace `VM_IP` below:
|
||||
|
||||
```console
|
||||
ssh root@VM_IP 'cat /etc/ssh/ssh_host_ed25519_key.pub' | \
|
||||
nix shell nixpkgs#ssh-to-age --command ssh-to-age
|
||||
```
|
||||
|
||||
This prints an `age1...` recipient; it does not copy the private key. Add the
|
||||
recipient to `.sops.yaml`:
|
||||
|
||||
```yaml
|
||||
- &system_portal_1 age1...
|
||||
```
|
||||
|
||||
Then add `*system_portal_1` to the age recipients for
|
||||
`users/secrets.yaml`. Re-encrypt the existing file for the new recipient and
|
||||
add the Tailscale key:
|
||||
|
||||
```console
|
||||
nix shell nixpkgs#sops --command sops updatekeys users/secrets.yaml
|
||||
nix shell nixpkgs#sops --command sops users/secrets.yaml
|
||||
```
|
||||
|
||||
Add the OAuth client secret from the `Auth Keys: Write` credential in the SOPS
|
||||
editor and save it:
|
||||
|
||||
```yaml
|
||||
tailscale_auth_key: tskey-client-...
|
||||
```
|
||||
|
||||
## Deploy
|
||||
|
||||
From the repository root, replace `VM_IP` with the VM's public IP:
|
||||
|
||||
```console
|
||||
nix run github:nix-community/nixos-anywhere -- \
|
||||
--copy-host-keys --flake .#portal_1 root@VM_IP
|
||||
```
|
||||
|
||||
This repartitions `/dev/vda`, so anything already on that disk is erased. The
|
||||
layout reserves 8 GiB for swap and assigns the remaining space to the root
|
||||
filesystem.
|
||||
|
||||
`--copy-host-keys` preserves the same private SSH host key at
|
||||
`/etc/ssh/ssh_host_ed25519_key` on the installed system. SOPS-Nix converts that
|
||||
key to an age identity during activation. After the reboot, connect as
|
||||
`richie` and verify that automatic Tailscale enrollment succeeded:
|
||||
|
||||
```console
|
||||
ssh -p 278 richie@VM_IP
|
||||
sudo tailscale status
|
||||
```
|
||||
|
||||
The installed OpenSSH service listens on port 278. Port 22 is served by
|
||||
Endlessh and will not provide an SSH login.
|
||||
|
||||
HAProxy uses the same frontend, routing, and rate-limiting configuration as
|
||||
Jeeves. Portal manages the ACME certificates for the existing public domains;
|
||||
their DNS records must resolve to Portal for HTTP-01 issuance and renewal.
|
||||
|
||||
The application backends still use Jeeves' original `127.0.0.1` addresses.
|
||||
Replace them with the corresponding Tailscale addresses before directing
|
||||
application traffic through Portal. Ports 80 and 443 are allowed through the
|
||||
firewall.
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
inputs,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
imports = [
|
||||
inputs.disko.nixosModules.disko
|
||||
"${inputs.self}/users/richie"
|
||||
"${inputs.self}/common/global"
|
||||
"${inputs.self}/common/optional/tailscale.nix"
|
||||
./disk-config.nix
|
||||
./haproxy
|
||||
];
|
||||
|
||||
nixpkgs.hostPlatform = "x86_64-linux";
|
||||
|
||||
boot = {
|
||||
# Avoid consuming the VM's limited memory for /tmp.
|
||||
tmp.useTmpfs = false;
|
||||
|
||||
# The Vultr system disk and NIC are exposed as virtio devices.
|
||||
initrd.availableKernelModules = [
|
||||
"virtio_pci"
|
||||
"virtio_blk"
|
||||
"virtio_scsi"
|
||||
"sd_mod"
|
||||
];
|
||||
};
|
||||
|
||||
networking = {
|
||||
hostName = "portal-1";
|
||||
useDHCP = lib.mkDefault true;
|
||||
|
||||
firewall = {
|
||||
enable = true;
|
||||
allowedTCPPorts = [ 278 ];
|
||||
};
|
||||
};
|
||||
|
||||
services.openssh.ports = [ 278 ];
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
curl
|
||||
htop
|
||||
vim
|
||||
];
|
||||
|
||||
# The VM uses its regular swap instead of compressed RAM swap.
|
||||
zramSwap.enable = false;
|
||||
|
||||
time.timeZone = "Etc/UTC";
|
||||
|
||||
system.stateVersion = "24.05";
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{ ... }:
|
||||
{
|
||||
# Vultr's first virtio disk is normally /dev/vda. Change this before
|
||||
# deployment if the selected image exposes its system disk differently.
|
||||
disko.devices.disk.main = {
|
||||
type = "disk";
|
||||
device = "/dev/vda";
|
||||
content = {
|
||||
type = "gpt";
|
||||
partitions = {
|
||||
bios = {
|
||||
size = "1M";
|
||||
type = "EF02";
|
||||
};
|
||||
|
||||
ESP = {
|
||||
size = "512M";
|
||||
type = "EF00";
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "vfat";
|
||||
mountpoint = "/boot";
|
||||
mountOptions = [
|
||||
"fmask=0077"
|
||||
"dmask=0077"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
swap = {
|
||||
size = "8G";
|
||||
content = {
|
||||
type = "swap";
|
||||
};
|
||||
};
|
||||
|
||||
root = {
|
||||
size = "100%";
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "ext4";
|
||||
mountpoint = "/";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Install GRUB for both legacy BIOS and EFI-capable Vultr plans.
|
||||
boot.loader = {
|
||||
grub = {
|
||||
enable = true;
|
||||
devices = [ "/dev/vda" ];
|
||||
efiSupport = true;
|
||||
efiInstallAsRemovable = true;
|
||||
};
|
||||
efi.canTouchEfiVariables = false;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
let
|
||||
domains = [
|
||||
"audiobookshelf"
|
||||
"cache"
|
||||
"gitea"
|
||||
"gems"
|
||||
"jellyfin"
|
||||
"share"
|
||||
];
|
||||
extraDomains = [ "www.norn-sight.com" ];
|
||||
|
||||
makeCert = name: {
|
||||
name = "${name}.tmmworkshop.com";
|
||||
value = {
|
||||
webroot = "/var/lib/acme/.challenges";
|
||||
group = "acme";
|
||||
reloadServices = [ "haproxy.service" ];
|
||||
};
|
||||
};
|
||||
|
||||
makeExtraCert = name: {
|
||||
inherit name;
|
||||
value = {
|
||||
webroot = "/var/lib/acme/.challenges";
|
||||
group = "acme";
|
||||
reloadServices = [ "haproxy.service" ];
|
||||
};
|
||||
};
|
||||
|
||||
acmeServices =
|
||||
map (domain: "acme-${domain}.tmmworkshop.com.service") domains
|
||||
++ map (domain: "acme-${domain}.service") extraDomains;
|
||||
in
|
||||
{
|
||||
networking.firewall.allowedTCPPorts = [
|
||||
80
|
||||
443
|
||||
2223
|
||||
];
|
||||
|
||||
# Global robots.txt served by HAProxy for every vhost (see haproxy.cfg).
|
||||
environment.etc."haproxy/robots.txt".source = ./robots.txt;
|
||||
|
||||
services = {
|
||||
haproxy = {
|
||||
enable = true;
|
||||
config = builtins.readFile ./haproxy.cfg;
|
||||
};
|
||||
nginx = {
|
||||
enable = true;
|
||||
virtualHosts."acme-challenge" = {
|
||||
listen = [
|
||||
{
|
||||
addr = "127.0.0.1";
|
||||
port = 8402;
|
||||
}
|
||||
];
|
||||
locations."/.well-known/acme-challenge/" = {
|
||||
root = "/var/lib/acme/.challenges";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
security.acme = {
|
||||
acceptTerms = true;
|
||||
defaults.email = "Richie@tmmworkshop.com";
|
||||
certs = builtins.listToAttrs ((map makeCert domains) ++ (map makeExtraCert extraDomains));
|
||||
};
|
||||
|
||||
systemd = {
|
||||
services.haproxy = {
|
||||
# HAProxy needs the initial certificates and Tailscale connectivity before
|
||||
# it can serve requests to its backends.
|
||||
after = acmeServices ++ [ "tailscaled-autoconnect.service" ];
|
||||
wants = acmeServices ++ [ "tailscaled-autoconnect.service" ];
|
||||
};
|
||||
tmpfiles.rules = [
|
||||
"d /var/lib/acme/.challenges 0750 acme acme - -"
|
||||
"d /var/lib/acme/.challenges/.well-known 0750 acme acme - -"
|
||||
"d /var/lib/acme/.challenges/.well-known/acme-challenge 0750 acme acme - -"
|
||||
];
|
||||
};
|
||||
|
||||
users.users = {
|
||||
haproxy.extraGroups = [ "acme" ];
|
||||
nginx.extraGroups = [ "acme" ];
|
||||
};
|
||||
}
|
||||
@@ -110,6 +110,14 @@ frontend ContentSwitching
|
||||
use_backend gems if host_gems
|
||||
use_backend norn_sight if host_norn_sight
|
||||
|
||||
# Proxy Gitea's built-in SSH server over Tailscale to Jeeves. Port 22 remains
|
||||
# available to Endlessh on Portal.
|
||||
frontend GiteaSSH
|
||||
bind *:2223 v4v6
|
||||
mode tcp
|
||||
option tcplog
|
||||
default_backend gitea_ssh
|
||||
|
||||
# Stick-table only (no servers): tracks per-IP request rate to Gitea's compare
|
||||
# endpoint over a 5-minute window so the frontend can cap it at 1 per 5 min.
|
||||
backend st_compare
|
||||
@@ -124,31 +132,35 @@ backend acme_challenge
|
||||
|
||||
backend audiobookshelf_nodes
|
||||
mode http
|
||||
server server 127.0.0.1:8000
|
||||
server server jeeves:8000
|
||||
|
||||
backend cache_nodes
|
||||
mode http
|
||||
server server 127.0.0.1:5000
|
||||
server server jeeves:5000
|
||||
|
||||
backend jellyfin
|
||||
mode http
|
||||
option forwardfor
|
||||
server jellyfin 127.0.0.1:8096
|
||||
server jellyfin jeeves:8096
|
||||
|
||||
backend share_nodes
|
||||
mode http
|
||||
server server 127.0.0.1:8091
|
||||
server server jeeves:8091
|
||||
|
||||
backend gitea
|
||||
mode http
|
||||
server server 127.0.0.1:6443
|
||||
server server jeeves:6443
|
||||
|
||||
backend gitea_ssh
|
||||
mode tcp
|
||||
server gitea jeeves:2223
|
||||
|
||||
backend gems
|
||||
mode http
|
||||
option forwardfor
|
||||
timeout server 1h
|
||||
server gems 127.0.0.1:8002
|
||||
server gems jeeves:8002
|
||||
|
||||
backend norn_sight
|
||||
mode http
|
||||
server server 127.0.0.1:8001
|
||||
server server jeeves:8001
|
||||
@@ -8,8 +8,10 @@
|
||||
"${inputs.self}/common/optional/steam.nix"
|
||||
"${inputs.self}/common/optional/syncthing_base.nix"
|
||||
"${inputs.self}/common/optional/systemd-boot.nix"
|
||||
"${inputs.self}/common/optional/tailscale.nix"
|
||||
"${inputs.self}/common/optional/yubikey.nix"
|
||||
"${inputs.self}/common/optional/zerotier.nix"
|
||||
"${inputs.self}/common/optional/zfs"
|
||||
./hardware.nix
|
||||
./open_webui.nix
|
||||
./programs.nix
|
||||
|
||||
@@ -1,22 +1,7 @@
|
||||
{ config, ... }:
|
||||
{
|
||||
imports = [
|
||||
./cli
|
||||
./minimal.nix
|
||||
./programs.nix
|
||||
./ssh_config.nix
|
||||
];
|
||||
|
||||
programs = {
|
||||
home-manager.enable = true;
|
||||
git.enable = true;
|
||||
};
|
||||
|
||||
home = {
|
||||
username = "richie";
|
||||
homeDirectory = "/home/${config.home.username}";
|
||||
stateVersion = "24.05";
|
||||
sessionVariables = {
|
||||
FLAKE = "$HOME/dotfiles";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{ config, pkgs, ... }:
|
||||
{
|
||||
imports = [
|
||||
./cli
|
||||
];
|
||||
|
||||
programs = {
|
||||
home-manager.enable = true;
|
||||
};
|
||||
|
||||
home = {
|
||||
username = "richie";
|
||||
homeDirectory = "/home/${config.home.username}";
|
||||
stateVersion = "24.05";
|
||||
sessionVariables.FLAKE = "$HOME/dotfiles";
|
||||
packages = with pkgs; [
|
||||
# cli
|
||||
btop
|
||||
eza
|
||||
ripgrep
|
||||
starship
|
||||
tmux
|
||||
# networking
|
||||
wget
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -3,8 +3,6 @@
|
||||
home.packages = with pkgs; [
|
||||
# cli
|
||||
bat
|
||||
btop
|
||||
eza
|
||||
fd
|
||||
ffmpegthumbnailer
|
||||
fzf
|
||||
@@ -18,9 +16,6 @@
|
||||
p7zip
|
||||
poppler
|
||||
rar
|
||||
ripgrep
|
||||
starship
|
||||
tmux
|
||||
unzip
|
||||
yazi
|
||||
zoxide
|
||||
@@ -37,7 +32,6 @@
|
||||
# networking
|
||||
iperf3
|
||||
nmap
|
||||
wget
|
||||
# python
|
||||
ruff
|
||||
uv
|
||||
|
||||
@@ -43,6 +43,18 @@
|
||||
IdentityFile = "~/.ssh/id_ed25519";
|
||||
Port = 922;
|
||||
};
|
||||
portal-1 = {
|
||||
HostName = "portal-1";
|
||||
User = "richie";
|
||||
IdentityFile = "~/.ssh/id_ed25519";
|
||||
Port = 278;
|
||||
};
|
||||
"gitea.tmmworkshop.com" = {
|
||||
HostName = "gitea.tmmworkshop.com";
|
||||
User = "gitea";
|
||||
IdentityFile = "~/.ssh/id_ed25519";
|
||||
Port = 2223;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
imports = [
|
||||
../home/minimal.nix
|
||||
];
|
||||
}
|
||||
+39
-37
@@ -1,62 +1,64 @@
|
||||
richie_password: ENC[AES256_GCM,data:DMi3M8aqrQ60APIofr8wJMh+VZ14hLRxz6jWZgzswr0pV/QVSX53ShBFr90ruO3mucOLYv0l+bI31covfqMAhXWBJp9wUgtC2Q==,iv:qgtn30hZfIL4dBnQSLkjbo7zPJA4m9TR0f52sTFc0v4=,tag:ydLbcGyXjv0fE+4b5ECX5w==,type:str]
|
||||
math_password: ENC[AES256_GCM,data:ykiSr3iBHrShJarEQSJ/zuXbCPcbW2oUpaAjblu1V15ufFKVSMZM94LlpMiCYtN9cYBLs98hcMeajJbvgbwT5emPHthy9+TJDw==,iv:1TJEUo0ishqFAZiUE1473yR3RT6Gbtqt4zM+C1a1KEk=,tag:pR6jyIj+bu3XaSx5yIHSmA==,type:str]
|
||||
tailscale_auth_key: ENC[AES256_GCM,data:SZEfpVnqA84AQLA/geGsPNZfsWBktlM3Zrm7OpuSS/hCWWEKtSIbRYyovH68h8tQJBQFpv18n4oXJe9eAr6Mog==,iv:x1m6XPS/h818qNRcO+uqYjgTfd/FkxHHaSHHJrLyrp4=,tag:eOpcJIhzUWFzXTuRdS9S6w==,type:str]
|
||||
tailscale_oauth_client_id: ENC[AES256_GCM,data:+cWxKlLIcJtsLhqmZUNLVDs=,iv:qrTjhLfu82pzbZnU6BrK1n+Ncbm2Cl3MMCSTKCJ6H8U=,tag:s/N5XpLwNH1gZ+LzbqRr2g==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBObHhkaFlnaG4zaTZtbkIw
|
||||
TkxSQnMxbDNwUVo4R1VYRDNKVFRDUE9kb0ZjCnpWWElJUVNuNFBsMzZod1ZQY0Fa
|
||||
VXgxNjZ0TUJ3cEt2OTFWL2dGY0txVHMKLS0tIHNQU3Rsanh4dkc3ZElsai9YMFdH
|
||||
cXUzVmFxTUVIOWZVR2Fpa2crdWsrdlkKwdGLfbKWc25qfBKyd/cawiUWv9iepKHN
|
||||
EOp/LdH2GbCfnQSVbxi28ukLHxWqOLdqMm8xSni/Of2PXvMnpdyCyQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEMjQvSzZSY08vRlI0dzBP
|
||||
YytpVlpPSGFHaTI5b2xzUkFFdEY0bVhQdGgwClM3UmM1OElpcEZTcTB0NHpSZ2d6
|
||||
aVBpcjVIaDk3Tko1cm4xYjdkYVQ1L1EKLS0tIE1aQW95U0JIVGc4MGloSXRXc3ZS
|
||||
a3EvU1dJdmxOeVBIS28yT3dHd2ZEYWcKv5TIqRRQs3AXER1WHEx8wotyXr2Ktxsu
|
||||
dwldwEHyDNqwf7UgGOZu7JueeZ7y2KC8KZ7ixIdrhVaoxuLEvCg7VA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1u8zj599elqqvcmhxn8zuwrufsz8w8w366d3ayrljjejljt2q45kq8mxw9c
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6R0djMTArVmFySE5DMnFr
|
||||
WFdBUERFbE1HRVVFd0oyaXJ2eU5HUStBUFN3CnR3ckZ2bkpGZFFScHQwTlBZYTMv
|
||||
cTBXbGVoTXlWczhId05QVjZDRzFqRjQKLS0tIDNIcVBydHYwTGRnVU1mWWk4WTlR
|
||||
eVlwQWgxSG5SdmFrWTlOcFo5eXZONWMKgx4huoSnbkRq0wQbsYgsWUKDTxDGNvYR
|
||||
anVMQg+c7PwDlk1V4JQZ4WrYLx63Ep5qDjGlN/Ssf2Vo6rAuuKetcA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByaE5jb3p3VzZ1dE05Wmlx
|
||||
c1ZoWGhTcTRHRE9oMWtkaCtBY1l1a2QyWWlvCk9MM2FmcEZubXR1REpOcHZ6aHVE
|
||||
UEkzbllZdkxPYzlTMHNod3VWNnhoaTgKLS0tIGVCdEpqUzhFM0swRjVjb0Z6ckFN
|
||||
UzlYb1U4UGJqWE9oczdlNVh1S1lqL3cKSKxaJFhun2ee45I1jC55YKNdfYB8MsS+
|
||||
dgFOFCvn/4uEcUyKZbUf0STjbIZ5eMHl8NVRTupI4tftGiE559M5Wg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1q47vup0tjhulkg7d6xwmdsgrw64h4ax3la3evzqpxyy4adsmk9fs56qz3y
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2QjZYejFYbCswQjdmaDA0
|
||||
L2ZqUUhtYU12YlpISmxueHQzRG5YL0tQNXh3CndGamMwRzYvUzkvaE9DVnMwTkNC
|
||||
cFMxczZuOUorS3dnVUpjaVdCUjhLZ2MKLS0tIGlDeGRGa3dRbWgzT3NVYjBaYUJ4
|
||||
VW5yeFlvWUZ5MVpNZHA5M1VXR1hxU1kKqii08/MB2aabgP4RQs1ry8AxmFqB8Mn+
|
||||
m7B0u64aziKXLSl0u471wqgD+YGRwNcajXT2pHCy8QWLznzvIMSrxA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2U01jTVZQT1RRRU1nQmVv
|
||||
Z0lnOGJLN1c0UkNLbnJjV29na3Rub0xzbXhNCjY0aFNuUnFDUnZheDRHT3R5L3Fv
|
||||
ZkJ3QkF2RENKRVdUYjRzRXMrNklvN2cKLS0tIFJzK2o1MmVTWHZMWmVDWlgzeGY5
|
||||
VDg4OGl0NGFYY2VNZGY5UkR1cVdWY0UK1qa3swdfgUw1tnnThRf1sl9f2zTP/d/d
|
||||
iwcenOEryXatVMJjZVWrLC5C7MwtKz+pIBXa98yaSpCDLqjAMwOCPg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1jhf7vm0005j60mjq63696frrmjhpy8kpc2d66mw044lqap5mjv4snmwvwm
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrSUc3VnllVDFZTm1jRnlP
|
||||
ZDBqelhkeHliZ1VlcjVnblQyeFlWclZTWkZjCjhJQk5EWkVoQjdoMHg4Zko4OU1C
|
||||
NWFnVTIwV3RYbTZSeWttaVRPbjNrMG8KLS0tIDZWUHJSbVVlR0lKUDRtc3VqOTc1
|
||||
cGJ3NDBLem9FNUpnbStYRTlqQStHV2sKwxPe4nTULsU0mVeUh8mhr2KX9U0iT5dL
|
||||
zvHldoQG6mZHgtHK6XI5AQJYf+zUW66OKqNSxAnn+BM20QkAQVZNVw==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTREI2UEZwS0Y0SmEvK1F4
|
||||
Y1hZZjRVWWxMT29mc3ZXcHFDclRUYi96LzA4CkIwZ2h4T2JQRW1OL2hmRTFHWkFY
|
||||
RHZqQUFqU3hvNlRBZHRiSThPVUE3TG8KLS0tIHh0Z2tlNXREczJsa1g2ekZyZjBt
|
||||
dW41aVA3VEVqa1NDMHM0T3FKZFNTN0kKGQPRUMSRtibFVFRZR/igsjgxgDo4ogga
|
||||
dUHBRU65r41er+Wk3lKdtbXJX1g83K8ay7540hNXheOhxFi57trrzA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age13lmqgc3jvkyah5e3vcwmj4s5wsc2akctcga0lpc0x8v8du3fxprqp4ldkv
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0NU9ac2FuRHI1dkQycmc0
|
||||
YlhGK29UeTdiZEZXcWtPUW4rMis4Z2NWYWpJCkVldEdMc3ZTaDFidHpaZk5mM283
|
||||
bTd0RlN5SHVabGhjeXFnSThydnVoem8KLS0tIElWclYvTmtkb0wyNDlKT3ZEckwx
|
||||
L2NObzZadlJ5d3MyeGRqKy95L3BOMFEKtoswi6r2TmCZzngUkiGQV5TTsuzisMFS
|
||||
5QI0aQZwhexqUMvbPuajYKvcPj+D6a2xaxbL3TBRLjOrFmcp5J7/YA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1l272y8udvg60z7edgje42fu49uwt4x2gxn5zvywssnv9h2krms8s094m4k
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByUlVjM2xpc281bHZzOGVo
|
||||
L3VEclhJZDd5SS9mazFiTk9DcmxMaWxPT213ClNPWERKQU03OWk0OEVIY05ib2VG
|
||||
WXhiZDhuMDZ5ZmFPWHB5RTFKYmpkVzAKLS0tIGppUndCb25wb2dyV01YbENrWjdU
|
||||
TmMvWVpobnl0eXBIOGQwMW5BSlhJTUkKzua1artJWbZlKfzv27xfZJeBpntBYwUf
|
||||
c8i1gNlvRwkhFAlrWcKR65vgyxsO3rbkLJRkcwG/q4hHj9zBeC/K2A==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrV1Z2MGNPK28wTStRSUZW
|
||||
dmFMaUt5MENHeUpVcDVxQnZhd1o5RmJwOWwwCittaGNrcW1hV25VaGJaTEhZRFBJ
|
||||
Y2toWEpFWFZYT0plb2lkTlJCS2Mwb3MKLS0tIHlkVjVESWFsbWlVQ0xVQ0llclF2
|
||||
L1FMbC91YjJsR3BGRjJ0QVQ1MDdBbEUKNnOI9+H1LJcvVcwl9uQMmFp35y5RB+Zl
|
||||
Mo3z3unweQtS+jiUn7RyVs5daG1E2tfihu2CQDWaktuCuunMpt3Wew==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ufnewppysaq2wwcl4ugngjz8pfzc5a35yg7luq0qmuqvctajcycs5lf6k4
|
||||
lastmodified: "2026-06-29T20:19:44Z"
|
||||
mac: ENC[AES256_GCM,data:GIvQxWt4tZGn0fyiXVtxGFQQoNcFUgilF+/PSz50exVrmzsS0XQUk/TIDFHaQR9jlJI50jqlyc1rBHgjnqC2oPHhPWaaVhgF18vQI55rGKdymNFjsHnaCkblFVdR1RJm0FSB2Ri6y5k8tfN3ywiwromJRz4NYzr1hbmr36azfg4=,iv:V3jspeYt/d2wy13gUrQmPGARm0hxwvSL/mocJAUofdw=,tag:vARoUzBWTJKkONDGoQdzNQ==,type:str]
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqR0hUT3hxTzRwTVhuTm9E
|
||||
TUFtNFFCYWNUNUNVbHNaNXQ0aEVBa04vSEU0CkRPVzhuYUsrM0wxZjRDdkxvTVA2
|
||||
UW9CSjlIQW5yenhaUi9mbEdLYlhWYk0KLS0tIGRXMm9Wa3l4UWxqcGl6ZlBZV3E0
|
||||
Z3YrbUYrSmV0NHNTeTdhMkJEdytwSmMKGLpe5+p4FGJNHy6SVgnsDbEsiP0J8lW/
|
||||
0+3X3LQoL0f1S2L5DjexF/H0NGvWjAPaGzDNB6V50VxGDabEIVqjvQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1vyav6kxtvt3z4vtnkkjj38eu8hlts5m7ygyckhskvalg2gpjk52su53d0a
|
||||
lastmodified: "2026-08-25T15:35:53Z"
|
||||
mac: ENC[AES256_GCM,data:4/WuSmUb2ONlB56dgbwwekZ8h7E1vvPzPxYCvNM8N68nt1nb9JxoMeAX9PktmljAarYLQJd1O5+49ZEph3Gzx9LrwvWnm9asq0UeeJi08x7wdQDcawNFlBFzXF2qlgxU1JssNHZZOB+5nYH4t8vFzPTq/9BqoifPR2+pLBYhUQI=,iv:N/gVgeWT2/1bfHuR6TanyDBijWmXAqyACXpnwfwDuVA=,tag:pQ4ct4G7SARz2wyjOc7AKQ==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.1
|
||||
version: 3.13.3
|
||||
|
||||
Reference in New Issue
Block a user