diff --git a/systems/portal_1/README.md b/systems/portal_1/README.md new file mode 100644 index 0000000..c7b1bbd --- /dev/null +++ b/systems/portal_1/README.md @@ -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. diff --git a/systems/portal_1/default.nix b/systems/portal_1/default.nix new file mode 100644 index 0000000..3231c6e --- /dev/null +++ b/systems/portal_1/default.nix @@ -0,0 +1,55 @@ +{ + inputs, + lib, + pkgs, + ... +}: +{ + imports = [ + inputs.disko.nixosModules.disko + "${inputs.self}/common/global" + "${inputs.self}/users/richie" + "${inputs.self}/common/optional/tailscale.nix" + ./disk-config.nix + ]; + + 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"; +} diff --git a/systems/portal_1/disk-config.nix b/systems/portal_1/disk-config.nix new file mode 100644 index 0000000..9e52244 --- /dev/null +++ b/systems/portal_1/disk-config.nix @@ -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; + }; +} diff --git a/users/richie/systems/portal-1.nix b/users/richie/systems/portal-1.nix new file mode 100644 index 0000000..b9d9b52 --- /dev/null +++ b/users/richie/systems/portal-1.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ../home/minimal.nix + ]; +}