From 59d742f4bac9d6215fa99af15e39f1d31897146a Mon Sep 17 00:00:00 2001 From: tyrolize Date: Tue, 16 Jun 2026 23:40:14 +0200 Subject: [PATCH] Initial infra scaffold: watcher live on Infomaniak Public Cloud - flake.nix exposes a devShell (openstackclient, opentofu, sops, age) plus nixosConfigurations.watcher (runtime) and packages.watcher-image (QCOW2 via nixos-generators / openstack format). - hosts/watcher/default.nix: SSH-only base, tyro user with key auth, root SSH disabled, trusted-users set so laptop closure pushes work. - modules/website.nix: Caddy serves tyrolize.ch from sites/tyrolize.ch/; lize.ch 301-redirects; firewall opens 80/443. Let's Encrypt via HTTP-01. - terraform/infomaniak/: OpenStack provider, security group (22/80/443), keypair, compute instance booted from the uploaded image. Auth via OS_* env vars sourced from terraform/infomaniak/.env by the devShell hook. - scripts/build-image.sh + scripts/deploy.sh. - dns/{tyrolize,lize}.ch.zone: full BIND zone files for the advanced view in Infomaniak DNS Manager; preserves kSuite mail records on lize.ch. Watcher live at 195.15.203.200 (IPv6 2001:1600:10:100::b4e), NixOS 25.05. HTTPS confirmed working on both domains. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 24 +++++++ .sops.yaml | 14 ++++ README.md | 84 +++++++++++++++++++++++ dns/lize.ch.zone | 26 ++++++++ dns/tyrolize.ch.zone | 27 ++++++++ flake.lock | 85 ++++++++++++++++++++++++ flake.nix | 64 ++++++++++++++++++ hosts/watcher/default.nix | 65 ++++++++++++++++++ modules/website.nix | 26 ++++++++ scripts/build-image.sh | 46 +++++++++++++ scripts/deploy.sh | 45 +++++++++++++ sites/tyrolize.ch/index.html | 24 +++++++ terraform/infomaniak/.env.example | 43 ++++++++++++ terraform/infomaniak/.terraform.lock.hcl | 24 +++++++ terraform/infomaniak/main.tf | 13 ++++ terraform/infomaniak/outputs.tf | 14 ++++ terraform/infomaniak/variables.tf | 36 ++++++++++ terraform/infomaniak/watcher.tf | 73 ++++++++++++++++++++ 18 files changed, 733 insertions(+) create mode 100644 .gitignore create mode 100644 .sops.yaml create mode 100644 README.md create mode 100644 dns/lize.ch.zone create mode 100644 dns/tyrolize.ch.zone create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 hosts/watcher/default.nix create mode 100644 modules/website.nix create mode 100755 scripts/build-image.sh create mode 100755 scripts/deploy.sh create mode 100644 sites/tyrolize.ch/index.html create mode 100644 terraform/infomaniak/.env.example create mode 100644 terraform/infomaniak/.terraform.lock.hcl create mode 100644 terraform/infomaniak/main.tf create mode 100644 terraform/infomaniak/outputs.tf create mode 100644 terraform/infomaniak/variables.tf create mode 100644 terraform/infomaniak/watcher.tf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62efe91 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Secrets — never commit +.env +clouds.yaml +*.tfvars +*.pem +*.key + +# Terraform local state and cache +terraform/**/.terraform/ +terraform/**/terraform.tfstate +terraform/**/terraform.tfstate.backup +terraform/**/.terraform.tfstate.lock.info + +# Nix build outputs +result +result-* + +# Editor / OS cruft +*.swp +.direnv/ +.DS_Store + +# Local PDFs in repo root (Infomaniak invoices, etc.) +*.pdf diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..e18a848 --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,14 @@ +# 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. + +keys: + - &laptop age1REPLACE_ME_WITH_YOUR_LAPTOP_PUBLIC_KEY + +creation_rules: + - path_regex: hosts/watcher/secrets\.yaml$ + key_groups: + - age: [*laptop] diff --git a/README.md b/README.md new file mode 100644 index 0000000..83a8119 --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# remote_server + +Personal infrastructure on **Infomaniak Public Cloud** (OpenStack), end-to-end +Swiss. A small always-on **watcher** VM plus, later, on-demand workers using +the same OpenStack APIs. + +## Layout + +``` +. +├── flake.nix # NixOS configs + watcher QCOW2 image package +├── hosts/ +│ └── watcher/ +│ └── default.nix # services, users, SSH; boot/disk via openstack-* modules +├── terraform/ +│ └── infomaniak/ # OpenStack provider, security group, instance +│ ├── main.tf +│ ├── variables.tf +│ ├── watcher.tf +│ ├── outputs.tf +│ ├── clouds.yaml.example +│ └── .env.example +├── scripts/ +│ ├── build-image.sh # build QCOW2 → upload to Infomaniak Glance +│ └── deploy.sh # terraform apply → nixos-rebuild switch +└── .sops.yaml # secrets encryption rules +``` + +## First-time setup + +1. **Infomaniak Public Cloud project** — Manager → Public Cloud → create a + project. In the project's API access section, generate an + **application credential** and copy the ID + secret. +2. **Fill `.env`** (gitignored, lives only in the repo — nothing in `~/.config`): + ```bash + cp terraform/infomaniak/.env.example terraform/infomaniak/.env + $EDITOR terraform/infomaniak/.env + ``` + Paste `OS_APPLICATION_CREDENTIAL_ID`, `OS_APPLICATION_CREDENTIAL_SECRET`, + and `TF_VAR_ssh_public_key`. +3. **Enter the dev shell** — brings in `openstack`, `terraform`, `jq`, `sops` + and auto-loads `.env`: + ```bash + nix develop + ``` +4. **Smoke-test auth and confirm catalogue defaults match**: + ```bash + openstack token issue + openstack flavor list | grep a2_ram4_disk20_perf1 + openstack network list | grep ext-net1 + ``` +5. **Paste your SSH public key** into `hosts/watcher/default.nix` + (`users.users.tyro.openssh.authorizedKeys.keys`). +6. **Build & upload the watcher image** (~5 min the first time): + ```bash + ./scripts/build-image.sh + ``` +7. **Provision and deploy**: + ```bash + ./scripts/deploy.sh + ``` + +After the script finishes: `ssh tyro@`. + +## Routine updates + +From inside `nix develop`, edit the flake, then push: + +```bash +nixos-rebuild switch --flake .#watcher --target-host tyro@ --use-remote-sudo +``` + +You only need to re-run `build-image.sh` if you want **fresh boots** to start +from a current image (e.g. after a major NixOS bump). + +## DNS + +Managed at Infomaniak (registrar). After the watcher has IPs: + +- `tyrolize.ch` A/AAAA → watcher +- `*.tyrolize.ch` A/AAAA → watcher (wildcard for subdomains served by Caddy) +- `lize.ch` A/AAAA → watcher (for the 301 redirect to tyrolize.ch) +- `lize.ch` MX/SPF/DKIM/DMARC → Infomaniak kSuite (auto-configured) +- `tyrolize.ch` empty-SPF + DMARC `p=reject` (anti-spoofing on non-mail domain) diff --git a/dns/lize.ch.zone b/dns/lize.ch.zone new file mode 100644 index 0000000..fc0a05a --- /dev/null +++ b/dns/lize.ch.zone @@ -0,0 +1,26 @@ +; Domain: lize.ch +; All existing kSuite mail records preserved exactly as-is. +; Adds: A/AAAA for the apex (Caddy on the watcher 301-redirects to +; https://tyrolize.ch), plus CAA restricting TLS to Let's Encrypt. + +$TTL 3600 + +@ IN SOA ns11.infomaniak.ch. hostmaster.infomaniak.ch. (2026061610 10800 3600 605800 3600) +@ 3600 IN NS ns11.infomaniak.ch. +@ 3600 IN NS ns12.infomaniak.ch. + +; --- kSuite mail (DO NOT TOUCH) --- +@ 3600 IN MX 5 mta-gw.infomaniak.ch. +@ 3600 IN TXT "v=spf1 include:spf.infomaniak.ch -all" +autoconfig 3600 IN CNAME infomaniak.com. +autodiscover 3600 IN CNAME infomaniak.com. +_dmarc 3600 IN TXT "v=DMARC1; p=reject;" +_domainkey 3600 IN NS ns11.infomaniak.ch. +_domainkey 3600 IN NS ns12.infomaniak.ch. + +; --- New: apex points at watcher VM for the redirect to tyrolize.ch --- +@ 300 IN A 195.15.203.200 +@ 300 IN AAAA 2001:1600:10:100::b4e + +; --- Restrict TLS cert issuance to Let's Encrypt --- +@ 3600 IN CAA 0 issue "letsencrypt.org" diff --git a/dns/tyrolize.ch.zone b/dns/tyrolize.ch.zone new file mode 100644 index 0000000..31b279f --- /dev/null +++ b/dns/tyrolize.ch.zone @@ -0,0 +1,27 @@ +; Domain: tyrolize.ch +; Adds A/AAAA + wildcard pointing at the watcher VM (195.15.203.200), +; plus anti-spoofing (no mail leaves this domain) and CAA restricting +; TLS issuance to Let's Encrypt. + +$TTL 3600 + +@ IN SOA ns11.infomaniak.ch. hostmaster.infomaniak.ch. (2026061646 10800 3600 605800 3600) +@ 3600 IN NS ns11.infomaniak.ch. +@ 3600 IN NS ns12.infomaniak.ch. + +; --- Watcher VM (short TTL during bring-up; raise to 3600 later if you want) --- +@ 300 IN A 195.15.203.200 +@ 300 IN AAAA 2001:1600:10:100::b4e +* 300 IN A 195.15.203.200 +* 300 IN AAAA 2001:1600:10:100::b4e + +; --- Anti-spoofing: no mail is sent from tyrolize.ch --- +@ 3600 IN TXT "v=spf1 -all" +_dmarc 3600 IN TXT "v=DMARC1; p=reject; rua=mailto:tyro@lize.ch" + +; --- Restrict TLS cert issuance to Let's Encrypt --- +@ 3600 IN CAA 0 issue "letsencrypt.org" + +; --- Infomaniak default (harmless on non-mail domain) --- +_domainkey 3600 IN NS ns11.infomaniak.ch. +_domainkey 3600 IN NS ns12.infomaniak.ch. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..c7db0a2 --- /dev/null +++ b/flake.lock @@ -0,0 +1,85 @@ +{ + "nodes": { + "nixlib": { + "locked": { + "lastModified": 1736643958, + "narHash": "sha256-tmpqTSWVRJVhpvfSN9KXBvKEXplrwKnSZNAoNPf/S/s=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "1418bc28a52126761c02dd3d89b2d8ca0f521181", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixos-generators": { + "inputs": { + "nixlib": "nixlib", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1769813415, + "narHash": "sha256-nnVmNNKBi1YiBNPhKclNYDORoHkuKipoz7EtVnXO50A=", + "owner": "nix-community", + "repo": "nixos-generators", + "rev": "8946737ff703382fda7623b9fab071d037e897d5", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixos-generators", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1767313136, + "narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixos-generators": "nixos-generators", + "nixpkgs": "nixpkgs", + "sops-nix": "sops-nix" + } + }, + "sops-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1780547341, + "narHash": "sha256-Gq8KNx5A7hBB3uGJaj6eQfLDIz5YdLu92gqBcvHvoUo=", + "owner": "Mic92", + "repo": "sops-nix", + "rev": "9ed65852b6257fbeae4355bc24ecfea307ca759a", + "type": "github" + }, + "original": { + "owner": "Mic92", + "repo": "sops-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..048aaf9 --- /dev/null +++ b/flake.nix @@ -0,0 +1,64 @@ +{ + description = "Personal infra on Infomaniak Public Cloud (OpenStack)"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; + + sops-nix.url = "github:Mic92/sops-nix"; + sops-nix.inputs.nixpkgs.follows = "nixpkgs"; + + nixos-generators.url = "github:nix-community/nixos-generators"; + nixos-generators.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = { self, nixpkgs, sops-nix, nixos-generators, ... }: + let + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages.${system}; + in { + # `nix develop` to enter a shell with every tool this repo needs. + devShells.${system}.default = pkgs.mkShell { + packages = with pkgs; [ + openstackclient # `openstack` CLI + opentofu # `tofu` — drop-in for terraform, OSS license + jq + age + sops # for sops-nix secrets + ]; + shellHook = '' + if [ -f "$PWD/terraform/infomaniak/.env" ]; then + set -a; . "$PWD/terraform/infomaniak/.env"; set +a + echo "loaded terraform/infomaniak/.env" + fi + ''; + }; + + # Runtime config — what the watcher box actually IS. + # Push updates with: + # nixos-rebuild switch --flake .#watcher --target-host tyro@ --use-remote-sudo + nixosConfigurations.watcher = nixpkgs.lib.nixosSystem { + inherit system; + modules = [ + ({ modulesPath, ... }: { + imports = [ + "${modulesPath}/virtualisation/openstack-config.nix" + ]; + }) + sops-nix.nixosModules.sops + ./hosts/watcher + ]; + }; + + # Build a QCOW2 image of the watcher to upload to Infomaniak Glance: + # nix build .#watcher-image + # ls -lh result/nixos.qcow2 + packages.${system}.watcher-image = nixos-generators.nixosGenerate { + inherit system pkgs; + format = "openstack"; + modules = [ + sops-nix.nixosModules.sops + ./hosts/watcher + ]; + }; + }; +} diff --git a/hosts/watcher/default.nix b/hosts/watcher/default.nix new file mode 100644 index 0000000..a3713ea --- /dev/null +++ b/hosts/watcher/default.nix @@ -0,0 +1,65 @@ +{ config, lib, pkgs, ... }: { + imports = [ + ../../modules/website.nix + ]; + + # 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) + # Both are wired in flake.nix. + + system.stateVersion = "25.05"; + + networking.hostName = "watcher"; + networking.firewall = { + enable = true; + allowedTCPPorts = [ 22 ]; # services like 80/443 added as they come online + }; + + time.timeZone = "Europe/Zurich"; + i18n.defaultLocale = "en_US.UTF-8"; + + users.mutableUsers = false; + users.users.tyro = { + isNormalUser = true; + extraGroups = [ "wheel" ]; + openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHC7oEcIquy/HWSHjA9N62FVKA6js4aOWu9q41Qp3nNj tyrolize@nixos" + ]; + }; + + services.openssh = { + enable = true; + settings = { + PasswordAuthentication = false; + KbdInteractiveAuthentication = false; + # openstack-config.nix defaults this to "prohibit-password" so cloud-init can + # inject the OpenStack keypair into root. We don't need it: the same key is + # already in users.users.tyro via the flake. Hard-disable root SSH. + PermitRootLogin = lib.mkForce "no"; + }; + }; + + security.sudo.wheelNeedsPassword = false; + + environment.systemPackages = with pkgs; [ + vim + git + htop + tmux + curl + wget + ]; + + nix.settings = { + experimental-features = [ "nix-command" "flakes" ]; + # Allow the tyro user to push pre-built closures from the laptop via + # `nixos-rebuild --target-host` without re-signing every store path. + trusted-users = [ "root" "@wheel" ]; + }; + nix.gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 14d"; + }; +} diff --git a/modules/website.nix b/modules/website.nix new file mode 100644 index 0000000..3f3f60f --- /dev/null +++ b/modules/website.nix @@ -0,0 +1,26 @@ +{ config, lib, pkgs, ... }: { + # Caddy serves tyrolize.ch and 301-redirects lize.ch to it. + # TLS certs auto-provisioned via Let's Encrypt (HTTP-01 challenge), which + # requires the apex DNS A/AAAA records to already point at this VM. + + networking.firewall.allowedTCPPorts = [ 80 443 ]; + + services.caddy = { + enable = true; + email = "tyro@lize.ch"; # Let's Encrypt account contact + + virtualHosts."tyrolize.ch" = { + extraConfig = '' + root * ${../sites/tyrolize.ch} + file_server + encode gzip zstd + ''; + }; + + virtualHosts."lize.ch" = { + extraConfig = '' + redir https://tyrolize.ch{uri} permanent + ''; + }; + }; +} diff --git a/scripts/build-image.sh b/scripts/build-image.sh new file mode 100755 index 0000000..17b6f75 --- /dev/null +++ b/scripts/build-image.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Build the watcher's NixOS QCOW2 image and upload it to Infomaniak Glance. +# +# Prereqs: +# - Run inside `nix develop` (provides openstackclient + nix + flakes). +# - terraform/infomaniak/.env populated and auto-sourced by the devShell. +# - SSH pubkey pasted into hosts/watcher/default.nix. +# +# Re-run any time the watcher's NixOS config changes if you want fresh boots +# from a current image. Routine config updates after boot use nixos-rebuild +# instead (no image rebuild required). + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +IMAGE_NAME="${TF_VAR_watcher_image_name:-nixos-watcher}" + +: "${OS_AUTH_URL:?terraform/infomaniak/.env not loaded — run inside 'nix develop'}" + +echo "==> building QCOW2 (this takes a few minutes the first time)" +nix build .#watcher-image + +# nixos-generators names the file with the channel revision, e.g. +# nixos-image-openstack-25.05.20260102.ac62194-x86_64-linux.qcow2 +QCOW=$(ls "$(readlink -f result)"/*.qcow2 | head -1) +[ -f "$QCOW" ] || { echo "no qcow2 found under result/" >&2; exit 1; } +ls -lh "$QCOW" + +echo "==> uploading to Infomaniak Glance as '$IMAGE_NAME'" +# If an older copy exists, delete it first (Glance image names aren't unique). +if openstack image show "$IMAGE_NAME" -f value -c id >/dev/null 2>&1; then + echo " older image found — deleting" + openstack image delete "$IMAGE_NAME" +fi + +openstack image create \ + --disk-format qcow2 \ + --container-format bare \ + --file "$QCOW" \ + --progress \ + "$IMAGE_NAME" + +echo "==> done. Image:" +openstack image show "$IMAGE_NAME" -f table -c id -c name -c status -c size diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..2cf8bed --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Provision (or update) the watcher instance via OpenTofu, then push the +# latest flake config with nixos-rebuild. +# +# Prereqs: +# - Watcher image already uploaded to Glance (run scripts/build-image.sh once) +# - Run inside `nix develop` (auto-sources terraform/infomaniak/.env) +# +# Idempotent: safe to re-run. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +: "${OS_AUTH_URL:?terraform/infomaniak/.env not loaded — run inside 'nix develop'}" +: "${TF_VAR_ssh_public_key:?TF_VAR_ssh_public_key missing — paste pubkey into .env}" + +echo "==> tofu apply" +pushd terraform/infomaniak >/dev/null +tofu init -upgrade +tofu apply -auto-approve +IPV4=$(tofu output -raw watcher_ipv4) +popd >/dev/null + +echo "==> watcher at $IPV4" + +echo "==> waiting for SSH" +for _ in $(seq 1 60); do + if ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=3 \ + -o BatchMode=yes tyro@"$IPV4" true 2>/dev/null; then + break + fi + sleep 2 +done + +echo "==> nixos-rebuild switch" +nixos-rebuild switch \ + --flake "$REPO_ROOT#watcher" \ + --target-host "tyro@$IPV4" \ + --use-remote-sudo + +echo +echo "Done." +echo " ssh tyro@$IPV4" diff --git a/sites/tyrolize.ch/index.html b/sites/tyrolize.ch/index.html new file mode 100644 index 0000000..5f8b693 --- /dev/null +++ b/sites/tyrolize.ch/index.html @@ -0,0 +1,24 @@ + + + + + + tyrolize + + + +

tyrolize.ch

+

Hello.

+

Mail: tyro@lize.ch

+ + diff --git a/terraform/infomaniak/.env.example b/terraform/infomaniak/.env.example new file mode 100644 index 0000000..a64a59e --- /dev/null +++ b/terraform/infomaniak/.env.example @@ -0,0 +1,43 @@ +# Copy to `.env` (gitignored) and fill in real values from the clouds.yaml +# you can download at: Infomaniak Manager → Public Cloud → your project → +# API access → Download clouds.yaml. +# +# Once filled in, just run `nix develop` from the repo root — the devShell's +# shellHook automatically sources this file. Then `openstack ...` and +# `terraform ...` are authenticated. + +# --- OpenStack auth (application credential = modern, recommended) --- +# In your downloaded clouds.yaml these live under: +# clouds.openstack.auth_type +# clouds.openstack.auth.auth_url +# clouds.openstack.auth.application_credential_id +# clouds.openstack.auth.application_credential_secret +export OS_AUTH_TYPE=v3applicationcredential +export OS_AUTH_URL=https://api.pub1.infomaniak.cloud/identity/v3 +export OS_APPLICATION_CREDENTIAL_ID=REPLACE_ME +export OS_APPLICATION_CREDENTIAL_SECRET=REPLACE_ME + +export OS_REGION_NAME=dc3-a +export OS_INTERFACE=public +export OS_IDENTITY_API_VERSION=3 + +# --- Alternative: legacy username + password auth --- +# Only if your clouds.yaml uses passwords instead of an application credential. +# Comment out the four OS_AUTH_TYPE / OS_AUTH_URL / OS_APPLICATION_* lines above +# and uncomment these instead: +# +# export OS_AUTH_URL=https://api.pub1.infomaniak.cloud/identity/v3 +# export OS_PROJECT_ID=REPLACE_ME +# export OS_PROJECT_NAME=REPLACE_ME +# export OS_USER_DOMAIN_NAME=Default +# export OS_USERNAME=REPLACE_ME +# export OS_PASSWORD=REPLACE_ME + +# --- Terraform variables --- +# Contents of ~/.ssh/id_ed25519.pub (one line, quoted). +export TF_VAR_ssh_public_key="ssh-ed25519 AAAA... tyro@lize.ch" + +# Override defaults from variables.tf only if you want a different size/network: +# export TF_VAR_flavor_name="a2-ram4-disk50-perf1" +# export TF_VAR_external_network="ext-net1" +# export TF_VAR_watcher_image_name="nixos-watcher" diff --git a/terraform/infomaniak/.terraform.lock.hcl b/terraform/infomaniak/.terraform.lock.hcl new file mode 100644 index 0000000..2582b89 --- /dev/null +++ b/terraform/infomaniak/.terraform.lock.hcl @@ -0,0 +1,24 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/terraform-provider-openstack/openstack" { + version = "2.1.0" + constraints = "~> 2.0" + hashes = [ + "h1:2TcmfEzBOGQPALErrXTaL6v+k/WAL40adao4izRYmdw=", + "zh:113661750398bf21c8fe36aade9fb6f5eb82b5bcd3bcd30bd37ac805d83398f4", + "zh:1b3c26347b9cd61e413ee93c2f422cc3278a77f55fd3516eaabb3e2a85f65281", + "zh:1b751bbf1e4152829a643b532fd3f5967a2e89a41fac381257e0b41665be3306", + "zh:1b967bbfd9b344419c0e0df0c3a15fcbd731e91f19a18955a55aace8d9ec039a", + "zh:1bc0fc7c0a21e568db043b654501ce668ba19bf7628d37a7d2aaa512fd6e5aeb", + "zh:425cbf61757d4b503e7bf0f409ea59835ca3afbd2432d56ad552c2e5d234a572", + "zh:67d4f059cb4d73bf6c060313ec32962c4e5bd8dc7be2542a6f2098ab32575cd9", + "zh:7fe841ac5b68a4f52fb3cf45070828f3845de44746679d434e4349f3c23e3ef2", + "zh:ac1ed4c6ef0b6a3410568a05d3f9933d184497f065988503c43da0b2f0786ab2", + "zh:c5c0d14c86fabd9ab6a5d555e6a8d511942665fb5fa948dd452b0d1934068344", + "zh:c9ae5c210192275185d6823566a9421983e8e64c2665a4cae00b92dd0706bd19", + "zh:ee9865ccc053e7f345e532654fb628d1cf1e81cd2e929643c1691bebffcf7b98", + "zh:f3416d2f666095e740522c4964e436470bb9ec17bd53aaae8169ad93297d07bd", + "zh:fbca85457dd49e17168989d64f7cfc4a519d55ef4e00e89cea2859e87ad87f83", + ] +} diff --git a/terraform/infomaniak/main.tf b/terraform/infomaniak/main.tf new file mode 100644 index 0000000..cbee7ea --- /dev/null +++ b/terraform/infomaniak/main.tf @@ -0,0 +1,13 @@ +terraform { + required_version = ">= 1.6" + required_providers { + openstack = { + source = "terraform-provider-openstack/openstack" + version = "~> 2.0" + } + } +} + +# Auth comes from OS_* env vars (sourced from terraform/infomaniak/.env +# via the devShell's shellHook). No clouds.yaml file needed. +provider "openstack" {} diff --git a/terraform/infomaniak/outputs.tf b/terraform/infomaniak/outputs.tf new file mode 100644 index 0000000..963efe6 --- /dev/null +++ b/terraform/infomaniak/outputs.tf @@ -0,0 +1,14 @@ +output "watcher_ipv4" { + value = openstack_compute_instance_v2.watcher.access_ip_v4 + description = "Public IPv4 of the watcher VM" +} + +output "watcher_ipv6" { + value = openstack_compute_instance_v2.watcher.access_ip_v6 + description = "Public IPv6 of the watcher VM" +} + +output "watcher_id" { + value = openstack_compute_instance_v2.watcher.id + description = "OpenStack instance ID (handy for `openstack server show`)" +} diff --git a/terraform/infomaniak/variables.tf b/terraform/infomaniak/variables.tf new file mode 100644 index 0000000..019078d --- /dev/null +++ b/terraform/infomaniak/variables.tf @@ -0,0 +1,36 @@ +variable "ssh_public_key" { + description = "Contents of ~/.ssh/id_ed25519.pub (one line, starts with 'ssh-ed25519 ...')" + type = string +} + +variable "ssh_key_name" { + description = "Display name for the keypair stored at Infomaniak" + type = string + default = "tyro-laptop" +} + +# Flavor naming (confirmed from Infomaniak's catalogue, 2026-06): +# a{vCPU}_ram{GB}_disk{GB}[_perf1] +# _perf1 = higher-tier SSD; disk0 = bring-your-own-volume. +# 2 vCPU / 4 GB / 20 GB SSD = a2-ram4-disk20-perf1, ~CHF 7.5/mo. + +variable "flavor_name" { + description = "OpenStack flavor for the watcher" + type = string + default = "a2-ram4-disk20-perf1" +} + +# ext-net1 is the dual-stack public network — VMs get IPv4 + IPv6 directly. +# ext-floating1 is only needed when running instances behind a router. + +variable "external_network" { + description = "Name of Infomaniak's public dual-stack network" + type = string + default = "ext-net1" +} + +variable "watcher_image_name" { + description = "Name of the NixOS image previously uploaded to Glance" + type = string + default = "nixos-watcher" +} diff --git a/terraform/infomaniak/watcher.tf b/terraform/infomaniak/watcher.tf new file mode 100644 index 0000000..ca7ecd0 --- /dev/null +++ b/terraform/infomaniak/watcher.tf @@ -0,0 +1,73 @@ +data "openstack_images_image_v2" "watcher" { + name = var.watcher_image_name + most_recent = true +} + +data "openstack_networking_network_v2" "external" { + name = var.external_network +} + +resource "openstack_compute_keypair_v2" "primary" { + name = var.ssh_key_name + public_key = var.ssh_public_key +} + +resource "openstack_networking_secgroup_v2" "watcher" { + name = "watcher" + description = "watcher: SSH + HTTP(S)" +} + +# SSH in (IPv4 + IPv6) +resource "openstack_networking_secgroup_rule_v2" "ssh_v4" { + direction = "ingress" + ethertype = "IPv4" + protocol = "tcp" + port_range_min = 22 + port_range_max = 22 + remote_ip_prefix = "0.0.0.0/0" + security_group_id = openstack_networking_secgroup_v2.watcher.id +} + +resource "openstack_networking_secgroup_rule_v2" "ssh_v6" { + direction = "ingress" + ethertype = "IPv6" + protocol = "tcp" + port_range_min = 22 + port_range_max = 22 + remote_ip_prefix = "::/0" + security_group_id = openstack_networking_secgroup_v2.watcher.id +} + +# HTTP(S) for Caddy. Open from day one so cert provisioning works the moment +# we add the website module — closing again would just be churn. +resource "openstack_networking_secgroup_rule_v2" "http" { + direction = "ingress" + ethertype = "IPv4" + protocol = "tcp" + port_range_min = 80 + port_range_max = 80 + remote_ip_prefix = "0.0.0.0/0" + security_group_id = openstack_networking_secgroup_v2.watcher.id +} + +resource "openstack_networking_secgroup_rule_v2" "https" { + direction = "ingress" + ethertype = "IPv4" + protocol = "tcp" + port_range_min = 443 + port_range_max = 443 + remote_ip_prefix = "0.0.0.0/0" + security_group_id = openstack_networking_secgroup_v2.watcher.id +} + +resource "openstack_compute_instance_v2" "watcher" { + name = "watcher" + flavor_name = var.flavor_name + image_id = data.openstack_images_image_v2.watcher.id + key_pair = openstack_compute_keypair_v2.primary.name + security_groups = [openstack_networking_secgroup_v2.watcher.name] + + network { + name = data.openstack_networking_network_v2.external.name + } +}