diff --git a/coding-rules.json b/coding-rules.json new file mode 100644 index 0000000..361a457 --- /dev/null +++ b/coding-rules.json @@ -0,0 +1 @@ +{"$schema":"https://opencode.ai/config.json","instructions":[".opencode-rules/concerns/coding-style.md",".opencode-rules/concerns/naming.md",".opencode-rules/concerns/documentation.md",".opencode-rules/concerns/testing.md",".opencode-rules/concerns/git-workflow.md",".opencode-rules/concerns/project-structure.md",".opencode-rules/languages/nix.md"]} diff --git a/docs/AGENIX-GUIDE.md b/docs/AGENIX-GUIDE.md new file mode 100644 index 0000000..0d888db --- /dev/null +++ b/docs/AGENIX-GUIDE.md @@ -0,0 +1,191 @@ +# Agenix Secret Management Guide + +A guide for creating and managing encrypted secrets using agenix in the nixos-config project. + +## Prerequisites + +- SSH key pair (ed25519 or rsa) in `~/.ssh/` +- Access to the nixos-config repository +- Secret added to `secrets.nix` with appropriate public keys + +--- + +## Creating a New Secret + +### Step 1: Add Secret to secrets.nix + +Edit `secrets.nix` and add a new entry: + +```nix +"secrets/.age".publicKeys = systems ++ users; +``` + +Where: +- `` is the desired filename (without `.age`) +- `systems` = hosts that can decrypt this secret +- `users` = users that can decrypt this secret + +Example: +```nix +"secrets/my-service-api-key.age".publicKeys = systems ++ users; +``` + +### Step 2: Create the Encrypted Secret + +Navigate to the nixos-config directory: + +```bash +cd ~/p/NIX/nixos-config +``` + +Generate a secure random token (if needed): + +```bash +head -c 32 /dev/urandom | base64 | tr -d '\n' +``` + +Or use a specific value: + +```bash +echo -n "your-secret-value-here" > /tmp/token.txt +``` + +Encrypt and create the secret file: + +```bash +cat /tmp/token.txt | RULES=./secrets.nix nix develop . --command sh -c 'agenix -e secrets/.age' +``` + +The `-e` flag encrypts stdin content into the age file. If no stdin is provided, agenix opens your editor. + +### Step 3: Verify the Secret + +Decrypt to verify: + +```bash +RULES=./secrets.nix nix develop . --command agenix -d secrets/.age +``` + +You should see your secret value printed to stdout. + +### Step 4: Use in NixOS Configuration + +Reference the secret in your service config: + +```nix +{ config, ... }: + +{ + # For environment files + environmentFiles = [ config.age.secrets."my-secret-name".path ]; + + # For file-based secrets + environmentFile = config.age.secrets."my-secret-name".path; +} +``` + +The secret will be available at `/run/agenix/` when the system builds. + +--- + +## Common Patterns + +### Token-Based Registration (e.g., Tuwunel) + +```nix +# secrets.nix +"secrets/tuwunel-registration-token.age".publicKeys = systems ++ users; +``` + +```nix +# services/tuwunel.nix +settings.global = { + allow_registration = true; + registration_token_file = config.age.secrets."tuwunel-registration-token".path; +}; +``` + +### API Keys via Environment Files + +```nix +# secrets.nix +"secrets/my-service-env.age".publicKeys = systems ++ users; +``` + +Create `my-service-env.age` containing: +``` +MY_SERVICE_API_KEY=your-key-here +DATABASE_URL=postgres://... +``` + +Reference in config: +```nix +environmentFiles = [ config.age.secrets."my-service-env".path ]; +``` + +### File-Based Secrets + +For binary files or specific file paths: + +```nix +settings = { + tls.cert = config.age.secrets."tls-cert".path; + tls.key = config.age.secrets."tls-key".path; +}; +``` + +--- + +## Agenix Command Reference + +| Command | Description | +|---------|-------------| +| `agenix -e ` | Edit/create encrypted secret (opens editor or uses stdin) | +| `agenix -d ` | Decrypt and print to stdout | +| `agenix -r` | Re-encrypt all secrets (after changing public keys) | +| `agenix --validate-config` | Validate secrets.nix syntax | + +### Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `RULES` | `./secrets.nix` | Path to secrets.nix file | +| `EDITOR` | `$EDITOR` | Editor for interactive editing | + +--- + +## Troubleshooting + +### "No identity found to decrypt" + +**Cause**: No SSH private key available. + +**Solution**: Ensure your private key is in `~/.ssh/`: +- `~/.ssh/id_rsa` +- `~/.ssh/id_ed25519` +- Or specify with `-i /path/to/private/key` + +### "Failed to find config root" + +**Cause**: Agenix can't find `secrets.nix`. + +**Solution**: Use `RULES=./secrets.nix` or run from the nixos-config directory. + +### Rekeying Secrets + +After adding new public keys to `secrets.nix`, re-encrypt all secrets: + +```bash +cd ~/p/NIX/nixos-config +RULES=./secrets.nix nix develop . --command agenix -r +``` + +--- + +## Security Notes + +- Never commit plaintext secrets to git +- Always use `secrets.nix` for encryption keys +- Include only necessary hosts/users in public keys +- Rotate secrets periodically +- Use `agenix -r` after modifying public keys diff --git a/flake.lock b/flake.lock index b3d5470..4f16f4b 100644 --- a/flake.lock +++ b/flake.lock @@ -26,11 +26,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1776092721, - "narHash": "sha256-avV4Snqp0K57I9s8D61+GHlg9DYZFSIvjaS4d4RYpG8=", + "lastModified": 1777053633, + "narHash": "sha256-AtoguTQc8x4ysH5KSlRaFMx1elTOnTdk1u4YtqlttVU=", "ref": "refs/heads/master", - "rev": "0ad41acb03eee0e22cba611b2171a3d3ee30cb10", - "revCount": 72, + "rev": "6e0e847299b81665ba594668fff208278bb3de3b", + "revCount": 73, "type": "git", "url": "https://code.m3ta.dev/m3tam3re/AGENTS" }, @@ -139,16 +139,16 @@ ] }, "locked": { - "lastModified": 1776182890, - "narHash": "sha256-+/VOe8XGq5klpU+I19D+3TcaR7o+Cwbq67KNF7mcFak=", - "owner": "Mic92", + "lastModified": 1776192490, + "narHash": "sha256-5gYQNEs0/vDkHhg63aHS5g0IwG/8HNvU1Vr00cElofk=", + "owner": "nix-community", "repo": "bun2nix", - "rev": "648d293c51e981aec9cb07ba4268bc19e7a8c575", + "rev": "6ef9f144616eedea90b364bb408ef2e1de7b310a", "type": "github" }, "original": { - "owner": "Mic92", - "ref": "catalog-support", + "owner": "nix-community", + "ref": "staging-2.1.0", "repo": "bun2nix", "type": "github" } @@ -280,21 +280,22 @@ "inputs": { "flake-parts": "flake-parts", "nixpkgs": "nixpkgs_3", + "npm-lockfile-fix": "npm-lockfile-fix", "pyproject-build-systems": "pyproject-build-systems", "pyproject-nix": "pyproject-nix_2", "uv2nix": "uv2nix_2" }, "locked": { - "lastModified": 1776369186, - "narHash": "sha256-+Kltn1Ar0Ye4iBc6UVwvNPGI0uIgnCktl4Obh964/60=", + "lastModified": 1776983519, + "narHash": "sha256-cJEYjf8xV4vDw9xRBh9SHMhamj5wNjEhmMO5O3s5lag=", "owner": "NousResearch", "repo": "hermes-agent", - "rev": "1dd6b5d5fb94cac59e93388f9aeee6bc365b8f42", + "rev": "bf196a3fc0fd1f79353369e8732051db275c6276", "type": "github" }, "original": { "owner": "NousResearch", - "ref": "v2026.4.16", + "ref": "v2026.4.23", "repo": "hermes-agent", "type": "github" } @@ -428,11 +429,11 @@ "treefmt-nix": "treefmt-nix" }, "locked": { - "lastModified": 1776870400, - "narHash": "sha256-PleMddBk0Vp2iVsNVgqmL00u1xX1cFx8FVYOEOur/1Q=", + "lastModified": 1777055188, + "narHash": "sha256-Cdo4+L4KTEBXCyJyZdXOjyXmnwl1m5VzHJ5uIwQTENE=", "owner": "numtide", "repo": "llm-agents.nix", - "rev": "bd0e8933483e6e1b33bd83c5e844926b33db0f75", + "rev": "bee1f681fda054c310cd25fbc944e02a7648a0ee", "type": "github" }, "original": { @@ -772,6 +773,27 @@ "type": "github" } }, + "npm-lockfile-fix": { + "inputs": { + "nixpkgs": [ + "hermes-agent", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1775903712, + "narHash": "sha256-2GV79U6iVH4gKAPWYrxUReB0S41ty/Y3dBLquU8AlaA=", + "owner": "jeslie0", + "repo": "npm-lockfile-fix", + "rev": "c6093acb0c0548e0f9b8b3d82918823721930fe8", + "type": "github" + }, + "original": { + "owner": "jeslie0", + "repo": "npm-lockfile-fix", + "type": "github" + } + }, "nur": { "inputs": { "flake-parts": "flake-parts_3", diff --git a/flake.nix b/flake.nix index 1f483e4..d7d5231 100644 --- a/flake.nix +++ b/flake.nix @@ -65,7 +65,7 @@ url = "github:vercel-labs/skills"; flake = false; }; - hermes-agent.url = "github:NousResearch/hermes-agent/v2026.4.16"; + hermes-agent.url = "github:NousResearch/hermes-agent/v2026.4.23"; }; outputs = { diff --git a/hosts/common/ports.nix b/hosts/common/ports.nix index 85ef345..f33fb13 100644 --- a/hosts/common/ports.nix +++ b/hosts/common/ports.nix @@ -38,7 +38,7 @@ kestra = 3018; outline = 3019; authentik = 3023; - conduit = 3024; + tuwunel = 3024; # Home automation homarr = 7575; diff --git a/hosts/common/users/m3tam3re.nix b/hosts/common/users/m3tam3re.nix index ae367e9..fb7607b 100644 --- a/hosts/common/users/m3tam3re.nix +++ b/hosts/common/users/m3tam3re.nix @@ -25,6 +25,7 @@ openssh.authorizedKeys.keys = [ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC3YEmpYbM+cpmyD10tzNRHEn526Z3LJOzYpWEKdJg8DaYyPbDn9iyVX30Nja2SrW4Wadws0Y8DW+Urs25/wVB6mKl7jgPJVkMi5hfobu3XAz8gwSdjDzRSWJrhjynuaXiTtRYED2INbvjLuxx3X8coNwMw58OuUuw5kNJp5aS2qFmHEYQErQsGT4MNqESe3jvTP27Z5pSneBj45LmGK+RcaSnJe7hG+KRtjuhjI7RdzMeDCX73SfUsal+rHeuEw/mmjYmiIItXhFTDn8ZvVwpBKv7xsJG90DkaX2vaTk0wgJdMnpVIuIRBa4EkmMWOQ3bMLGkLQeK/4FUkNcvQ/4+zcZsg4cY9Q7Fj55DD41hAUdF6SYODtn5qMPsTCnJz44glHt/oseKXMSd556NIw2HOvihbJW7Rwl4OEjGaO/dF4nUw4c9tHWmMn9dLslAVpUuZOb7ykgP0jk79ldT3Dv+2Hj0CdAWT2cJAdFX58KQ9jUPT3tBnObSF1lGMI7t77VU= m3tam3re@m3-nix" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBZcjCKl0DRuOUOMXbM0GKY5JjvmyFpVZ/tRlTKWu/zp razr" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEZbg/Z9mnflXuLahGY8WOSBMqbgeqVIkIwRkquys1Ml sascha.koenig@azintec.com" ]; packages = [inputs.home-manager.packages.${pkgs.stdenv.hostPlatform.system}.default]; }; diff --git a/hosts/m3-atlas/secrets.nix b/hosts/m3-atlas/secrets.nix index a4716bd..1a8e166 100644 --- a/hosts/m3-atlas/secrets.nix +++ b/hosts/m3-atlas/secrets.nix @@ -33,6 +33,10 @@ restreamer-env = {file = ../../secrets/restreamer-env.age;}; searx = {file = ../../secrets/searx.age;}; tailscale-key = {file = ../../secrets/tailscale-key.age;}; + tuwunel-registration-token = { + file = ../../secrets/tuwunel-registration-token.age; + owner = "tuwunel"; + }; traefik = { file = ../../secrets/traefik.age; owner = "traefik"; @@ -65,7 +69,6 @@ owner = "m3tam3re"; }; authentik-env = {file = ../../secrets/authentik-env.age;}; - conduit-env = {file = ../../secrets/conduit-env.age;}; }; }; } diff --git a/hosts/m3-atlas/services/default.nix b/hosts/m3-atlas/services/default.nix index 0e18ffb..806e523 100644 --- a/hosts/m3-atlas/services/default.nix +++ b/hosts/m3-atlas/services/default.nix @@ -1,6 +1,6 @@ { imports = [ - ./conduit.nix + ./tuwunel.nix ./containers ./gitea.nix ./gitea-actions-runner.nix diff --git a/hosts/m3-atlas/services/conduit.nix b/hosts/m3-atlas/services/tuwunel.nix similarity index 55% rename from hosts/m3-atlas/services/conduit.nix rename to hosts/m3-atlas/services/tuwunel.nix index 024531f..48facd2 100644 --- a/hosts/m3-atlas/services/conduit.nix +++ b/hosts/m3-atlas/services/tuwunel.nix @@ -1,44 +1,46 @@ -{config, ...}: { - services.matrix-conduit = { +{config, ...}: let + # Tuwunel uses a list for ports + tuwunel-port = config.m3ta.ports.get "tuwunel"; +in { + services.matrix-tuwunel = { enable = true; settings.global = { server_name = "m3ta.dev"; - address = "127.0.0.1"; - port = config.m3ta.ports.get "conduit"; + address = ["127.0.0.1"]; + port = [tuwunel-port]; max_request_size = 20000000; - allow_registration = false; + allow_registration = true; + registration_token_file = config.age.secrets."tuwunel-registration-token".path; allow_encryption = true; allow_federation = true; trusted_servers = ["matrix.org"]; - database_backend = "rocksdb"; }; - secretFile = config.age.secrets.conduit-env.path; }; - # Traefik configuration for Conduit + # Traefik configuration for Tuwunel services.traefik.dynamicConfigOptions.http = { - services.conduit.loadBalancer.servers = [ + services.tuwunel.loadBalancer.servers = [ { - url = "http://localhost:${toString (config.m3ta.ports.get "conduit")}/"; + url = "http://localhost:${toString tuwunel-port}/"; } ]; - routers.conduit = { + routers.tuwunel = { rule = "Host(`matrix.m3ta.dev`)"; tls = { certResolver = "godaddy"; }; - service = "conduit"; + service = "tuwunel"; entrypoints = "websecure"; }; # Federation endpoint on base domain - routers.conduit-federation = { + routers.tuwunel-federation = { rule = "Host(`m3ta.dev`) && PathPrefix(`/_matrix`)"; tls = { certResolver = "godaddy"; }; - service = "conduit"; + service = "tuwunel"; entrypoints = "websecure"; }; }; diff --git a/secrets.nix b/secrets.nix index 831e8f9..ee1fa9f 100644 --- a/secrets.nix +++ b/secrets.nix @@ -16,7 +16,6 @@ in { "secrets/anytype-key.age".publicKeys = systems ++ users; "secrets/anytype-key-ares.age".publicKeys = systems ++ users; "secrets/authentik-env.age".publicKeys = systems ++ users; - "secrets/conduit-env.age".publicKeys = systems ++ users; "secrets/baserow-env.age".publicKeys = systems ++ users; "secrets/ghost-env.age".publicKeys = systems ++ users; "secrets/littlelink-m3tam3re.age".publicKeys = systems ++ users; @@ -51,4 +50,5 @@ in { "secrets/honcho-key.age".publicKeys = systems ++ users; "secrets/hermes-env.age".publicKeys = systems ++ users; "secrets/hermes-cloud-env.age".publicKeys = systems ++ users; + "secrets/tuwunel-registration-token.age".publicKeys = systems ++ users; } diff --git a/secrets/conduit-env.age b/secrets/conduit-env.age deleted file mode 100644 index dded151..0000000 Binary files a/secrets/conduit-env.age and /dev/null differ diff --git a/secrets/tuwunel-registration-token.age b/secrets/tuwunel-registration-token.age new file mode 100644 index 0000000..ec0e266 --- /dev/null +++ b/secrets/tuwunel-registration-token.age @@ -0,0 +1,25 @@ +age-encryption.org/v1 +-> ssh-ed25519 4NLKrw u9nwBPCI+zx7fYqOzv/63Y4mBK/n5GWhWGaPuWeFhEg +3yCRXT+yzXNj+1VKxVu5N2+Ny0Q+AMS6t/zpZxSgONk +-> ssh-ed25519 5kwcsA v7wY7U6E/We+1jWZEfLDkktdYf+ghngJWv96R4TPtxw +kGoyKE32eqonVlE1Ur47t3kpwUtcQcdmSxBh8YFM2Ms +-> ssh-ed25519 9d4YIQ 0PH2mtTx4EmwrRh9+QxEmYgVpGzQ2vlsh1SzaBwHGGE +Um0pazZRjpfJFG11kib7fLV494OOyVXxrMAzoZ9JDD0 +-> ssh-ed25519 3Bcr1w /ikZTn/m+w7LlUx5iLbmLvH4gVAzLwR1qj97aJ5/CCc +bVQT83BVrousaRnAaOgw0w9cCOulCslT6WpLpAGWz1c +-> ssh-ed25519 c4NQlA slStbXPWuslSQftcqBE7qdQ+wF+rADcbyqUe7sC8ZxM +rJw7+fA8kncbBdbYWAhS+TlGVJK4l7dOaxLuUigRHuM +-> ssh-rsa DQlE7w +nk4lSYyM1mom5GJva11LGS8FnZX7OVk0tpC+H2mYqKgfmJm3yyFrZNUOKZHT/EtN +WpH5oHKWMhveBCPBB20JNNMg4hfJv4E3MfivXp2dABH/Y1vitG9pZKlp8KKsC6nC +K/OR6y50L57KcC3M0WJ1fVTfaITdzugBGr3UYEUERG+6lw9EJmnlef+6OLafPyJM +HslKUP54oSuMeI1Gdqne94nGjj8K/MFzOOxgIYVAGUayIK1RMiajKTfDo6E+irFn +FJnZn4sVpsvD5FXZbmXWOORKz3APA9NbwvmjEgT9TZz+aZKHgS2qyyE8VrPr8vHO +wBdlmKuV9U2DLSdFAIjwVXg1olarh4ZuC1+i/x6l7je6MYCwAMJwaUBnTNlBcXCg +4REScinoz9Cx+Rjuk9OjYFrrpjCXJEfKwaHXNHW04VxnMb7mQ2I3/EQ5/NnJ2PaC +vf8JdGaenSnRmTCj7977SmuN9dFIDnwvNkLxE3wAa8ziWbJeC4jeaIhE4uUp36gF + +-> ssh-ed25519 CSMyhg KpY+UDROmpjQSXrerSVADQlj/9rWWc4iD90vRM5aX3k +TBhiMdnx4WpH6t6TgVXfTQayyIrOCFabii96hFK0FY8 +--- gAkvzBVNmd7e5uSyyg4vS5cnObeZ2t2xgOiWrP55KK8 +?Žë 0žÎ¦›òxp¸a¡{ă9¦'Å¿e<ÕK„—“´¥¼¸SôÕøÅJ²?ö°~§0?:öðct$òØåJRËåì \ No newline at end of file