diff --git a/.github/skills/dev/testing/troubleshoot-lxd-instances/skill.md b/.github/skills/dev/testing/troubleshoot-lxd-instances/skill.md index 8b58e400b..1d21ba52f 100644 --- a/.github/skills/dev/testing/troubleshoot-lxd-instances/skill.md +++ b/.github/skills/dev/testing/troubleshoot-lxd-instances/skill.md @@ -184,5 +184,5 @@ lxc info - Emergency cleanup: use the `clean-lxd-environments` skill - Expected test errors: use the `debug-test-errors` skill -- LXD provider docs: [`docs/user-guide/providers/lxd.md`](../../../../docs/user-guide/providers/lxd.md) +- LXD provider docs: [`docs/user-guide/providers/lxd/`](../../../../docs/user-guide/providers/lxd/) - E2E troubleshooting: [`docs/e2e-testing/troubleshooting.md`](../../../../docs/e2e-testing/troubleshooting.md) diff --git a/docs/deployments/hetzner-demo-tracker/deployment-spec.md b/docs/deployments/hetzner-demo-tracker/deployment-spec.md index 262221881..fd07e0f04 100644 --- a/docs/deployments/hetzner-demo-tracker/deployment-spec.md +++ b/docs/deployments/hetzner-demo-tracker/deployment-spec.md @@ -160,7 +160,7 @@ The file is stored at `envs/torrust-tracker-demo.json` (git-ignored — contains ## Related Documentation -- [Hetzner server types and pricing](../../user-guide/providers/hetzner.md#available-server-types) +- [Hetzner server types and pricing](../../user-guide/providers/hetzner/#available-server-types) - [HTTPS/TLS configuration](../../user-guide/services/https.md) - [Grafana service](../../user-guide/services/grafana.md) - [Prometheus service](../../user-guide/services/prometheus.md) diff --git a/docs/deployments/hetzner-demo-tracker/prerequisites.md b/docs/deployments/hetzner-demo-tracker/prerequisites.md index e8c5b3d67..7b4e55868 100644 --- a/docs/deployments/hetzner-demo-tracker/prerequisites.md +++ b/docs/deployments/hetzner-demo-tracker/prerequisites.md @@ -202,6 +202,6 @@ torrust-tracker-demo.com. 3600 IN SOA hydrogen.ns.hetzner.com. dns.hetzner.com. ## Related Documentation -- [Hetzner Cloud Provider guide](../../user-guide/providers/hetzner.md) +- [Hetzner Cloud Provider guide](../../user-guide/providers/hetzner/) - [Quick Start: Docker Deployment](../../user-guide/quick-start/docker.md) - [Quick Start: Native Installation](../../user-guide/quick-start/native.md) diff --git a/docs/issues/405-deploy-hetzner-demo-tracker-and-document-process.md b/docs/issues/405-deploy-hetzner-demo-tracker-and-document-process.md index 5b2fbc2ca..cf59f93ab 100644 --- a/docs/issues/405-deploy-hetzner-demo-tracker-and-document-process.md +++ b/docs/issues/405-deploy-hetzner-demo-tracker-and-document-process.md @@ -2,7 +2,7 @@ **Issue**: #405 **Parent Epic**: None -**Related**: [docs/user-guide/providers/hetzner.md](../user-guide/providers/hetzner.md), [docs/user-guide/quick-start/docker.md](../user-guide/quick-start/docker.md) +**Related**: [docs/user-guide/providers/hetzner/](../user-guide/providers/hetzner/), [docs/user-guide/quick-start/docker.md](../user-guide/quick-start/docker.md) ## Overview @@ -118,7 +118,7 @@ See [`docs/deployments/hetzner-demo-tracker/post-provision/`](../deployments/het ## Related Documentation -- [Hetzner Cloud Provider](../user-guide/providers/hetzner.md) +- [Hetzner Cloud Provider](../user-guide/providers/hetzner/) - [Quick Start: Docker Deployment](../user-guide/quick-start/docker.md) - [Deployment Overview](../deployment-overview.md) - [User Guide](../user-guide/README.md) diff --git a/docs/issues/411-bug-ssh-key-passphrase-breaks-automated-deployment.md b/docs/issues/411-bug-ssh-key-passphrase-breaks-automated-deployment.md index 155870bb5..23b3c0f4f 100644 --- a/docs/issues/411-bug-ssh-key-passphrase-breaks-automated-deployment.md +++ b/docs/issues/411-bug-ssh-key-passphrase-breaks-automated-deployment.md @@ -210,7 +210,7 @@ Create `docs/user-guide/ssh-keys.md` covering: #### Update: Hetzner provider guide -`docs/user-guide/providers/hetzner.md` — add a "SSH Key Requirements" section or +`docs/user-guide/providers/hetzner/` — add a "SSH Key Requirements" section or callout box noting that Docker-based deployments require a passphrase-free key (or agent forwarding) and linking to the new SSH keys page. @@ -246,7 +246,7 @@ configured private key appears to be passphrase-protected. ### Phase 3: Documentation - [ ] Create `docs/user-guide/ssh-keys.md` covering all workflows and security notes -- [ ] Update `docs/user-guide/providers/hetzner.md` with an SSH key requirements note +- [ ] Update `docs/user-guide/providers/hetzner/` with an SSH key requirements note - [ ] Update `docs/user-guide/commands/create.md` to mention the passphrase warning - [ ] Update `docs/user-guide/README.md` to link to the new `ssh-keys.md` page @@ -276,11 +276,11 @@ configured private key appears to be passphrase-protected. forwarding, separate key) - [ ] `docs/user-guide/ssh-keys.md` exists and covers key requirements, workflows, and security notes -- [ ] `docs/user-guide/providers/hetzner.md` references the SSH key requirements +- [ ] `docs/user-guide/providers/hetzner/` references the SSH key requirements ## Related Documentation - [docs/deployments/hetzner-demo-tracker/commands/provision/problems.md](../deployments/hetzner-demo-tracker/commands/provision/problems.md) — root cause analysis and resolution for the Hetzner deployment failure - [src/adapters/ssh/ssh/credentials.rs](../../src/adapters/ssh/ssh/credentials.rs) — `SshCredentials` struct - [src/presentation/cli/controllers/create/subcommands/environment/handler.rs](../../src/presentation/cli/controllers/create/subcommands/environment/handler.rs) — where the warning is added -- [docs/user-guide/providers/hetzner.md](../user-guide/providers/hetzner.md) — Hetzner provider guide +- [docs/user-guide/providers/hetzner/](../user-guide/providers/hetzner/) — Hetzner provider guide diff --git a/docs/security/ssh-root-access-hetzner.md b/docs/security/ssh-root-access-hetzner.md index 913464053..b8413cbe0 100644 --- a/docs/security/ssh-root-access-hetzner.md +++ b/docs/security/ssh-root-access-hetzner.md @@ -131,5 +131,5 @@ Consider disabling root access after successful deployment: ## Related Documentation - [ADR: Hetzner SSH Key Dual Injection Pattern](../decisions/hetzner-ssh-key-dual-injection.md) -- [Hetzner Provider Documentation](../user-guide/providers/hetzner.md) +- [Hetzner Provider Documentation](../user-guide/providers/hetzner/) - [SSH Keys Guide](../tech-stack/ssh-keys.md) diff --git a/docs/tools/lxd-cleanup.md b/docs/tools/lxd-cleanup.md index c182e13e6..f62ce713f 100644 --- a/docs/tools/lxd-cleanup.md +++ b/docs/tools/lxd-cleanup.md @@ -240,4 +240,4 @@ cargo run --bin lxd_cleanup -- environment-name - [E2E Testing Documentation](../e2e-testing/README.md) - [Preflight Cleanup Implementation](../../src/testing/e2e/tasks/virtual_machine/preflight_cleanup.rs) -- [LXD Provider Documentation](../user-guide/providers/lxd.md) +- [LXD Provider Documentation](../user-guide/providers/lxd/) diff --git a/docs/user-guide/providers/README.md b/docs/user-guide/providers/README.md index f6e182f8c..df4bf313f 100644 --- a/docs/user-guide/providers/README.md +++ b/docs/user-guide/providers/README.md @@ -4,10 +4,10 @@ This directory contains provider-specific configuration guides. ## Available Providers -| Provider | Status | Description | -| --------------------------- | --------- | ------------------------------------------ | -| [LXD](lxd.md) | ✅ Stable | Local development using LXD containers/VMs | -| [Hetzner Cloud](hetzner.md) | 🆕 New | Cost-effective European cloud provider | +| Provider | Status | Description | +| ------------------------- | --------- | ------------------------------------------ | +| [LXD](lxd/) | ✅ Stable | Local development using LXD containers/VMs | +| [Hetzner Cloud](hetzner/) | 🆕 New | Cost-effective European cloud provider | ## Choosing a Provider @@ -30,7 +30,7 @@ To add a new provider: 1. Create OpenTofu templates in `templates/tofu//` 2. Add provider configuration types in `src/domain/provider/` 3. Update the template renderer for provider-specific logic -4. Add documentation in `docs/user-guide/providers/.md` +4. Add documentation in `docs/user-guide/providers//README.md` See the [contributing guide](../../contributing/README.md) for more details. @@ -39,3 +39,4 @@ See the [contributing guide](../../contributing/README.md) for more details. - [Quick Start Guides](../quick-start/README.md) - Docker and native installation guides - [Commands Reference](../commands/README.md) - Available commands - [SSH Keys](../../tech-stack/ssh-keys.md) - SSH key generation and management +- [Hetzner Post-Deployment](hetzner/post-deployment.md) - Manual steps for floating IPs and IPv6 diff --git a/docs/user-guide/providers/hetzner.md b/docs/user-guide/providers/hetzner/README.md similarity index 87% rename from docs/user-guide/providers/hetzner.md rename to docs/user-guide/providers/hetzner/README.md index 96c464d5c..ace3c9ba5 100644 --- a/docs/user-guide/providers/hetzner.md +++ b/docs/user-guide/providers/hetzner/README.md @@ -17,7 +17,7 @@ This guide covers Hetzner-specific configuration for cloud deployments. - Hetzner Cloud account ([sign up](https://www.hetzner.com/cloud)) - API token with read/write permissions -- SSH key pair (see [SSH keys guide](../../tech-stack/ssh-keys.md)) +- SSH key pair (see [SSH keys guide](../../../tech-stack/ssh-keys.md)) ## Create API Token @@ -143,7 +143,7 @@ cat /var/log/cloud-init-output.log 2. **Restrict SSH access** - Consider using Hetzner Firewall 3. **Use strong SSH keys** - Ed25519 or RSA 4096-bit minimum 4. **Regular updates** - Keep server packages updated -5. **Disable root SSH access** - For production, see [SSH Root Access Guide](../../security/ssh-root-access-hetzner.md) +5. **Disable root SSH access** - For production, see [SSH Root Access Guide](../../../security/ssh-root-access-hetzner.md) ## SSH Key Requirements @@ -163,13 +163,13 @@ private key will cause the `provision` step to fail with # Press Enter twice for an empty new passphrase ``` -2. **Forward your SSH agent** into the container (see [SSH Keys Guide](../ssh-keys.md#workflow-2--passphrase-protected-key-with-ssh-agent-forwarding-into-docker)). +2. **Forward your SSH agent** into the container (see [SSH Keys Guide](../../ssh-keys.md#workflow-2--passphrase-protected-key-with-ssh-agent-forwarding-into-docker)). The `create environment` command will warn you if it detects a passphrase-protected key so you can resolve this before reaching `provision`. For more detail on generating keys, removing passphrases, and security considerations, -see the [SSH Keys Guide](../ssh-keys.md). +see the [SSH Keys Guide](../../ssh-keys.md). ## SSH Key Behavior @@ -182,14 +182,15 @@ Hetzner deployments configure SSH access through two mechanisms: **Why both?** If cloud-init fails, root SSH access provides a debugging path. Without it, a failed cloud-init would leave the server completely inaccessible. -**For stricter security**: You can disable root SSH access after deployment. See [SSH Root Access on Hetzner](../../security/ssh-root-access-hetzner.md) for instructions. +**For stricter security**: You can disable root SSH access after deployment. See [SSH Root Access on Hetzner](../../../security/ssh-root-access-hetzner.md) for instructions. **Note**: The SSH key appears in your Hetzner Console under **Security** → **SSH Keys** with the name `torrust-tracker-vm--ssh-key`. ## Related Documentation -- [Quick Start: Docker](../quick-start/docker.md) - Deploy to Hetzner using Docker -- [Quick Start: Native](../quick-start/native.md) - Deploy using native installation -- [SSH Keys Guide](../../tech-stack/ssh-keys.md) - SSH key generation -- [SSH Root Access Security](../../security/ssh-root-access-hetzner.md) - Disabling root access -- [LXD Provider](lxd.md) - Local development alternative +- [Quick Start: Docker](../../quick-start/docker.md) - Deploy to Hetzner using Docker +- [Quick Start: Native](../../quick-start/native.md) - Deploy using native installation +- [SSH Keys Guide](../../../tech-stack/ssh-keys.md) - SSH key generation +- [SSH Root Access Security](../../../security/ssh-root-access-hetzner.md) - Disabling root access +- [LXD Provider](../lxd/) - Local development alternative +- [Post-Deployment: Floating IPs and IPv6](post-deployment.md) - Manual steps for floating IPs and IPv6 UDP diff --git a/docs/user-guide/providers/hetzner/post-deployment.md b/docs/user-guide/providers/hetzner/post-deployment.md new file mode 100644 index 000000000..479997474 --- /dev/null +++ b/docs/user-guide/providers/hetzner/post-deployment.md @@ -0,0 +1,231 @@ +# Hetzner Post-Deployment: Floating IPs and IPv6 + +This guide documents the manual steps required **after** running the deployer when the server uses +**Hetzner floating IPs** and/or needs **IPv6 UDP tracker support**. + +These steps are not planned to be automated by the deployer. They are specific to multi-IP setups +where separate floating IPs are used for separate tracker endpoints (e.g. one IP for the HTTP +tracker, one for the UDP tracker) so that both can be listed independently on +[newTrackon](https://newtrackon.com/), which tracks one tracker per IP. + +The reference implementation is +[torrust/torrust-tracker-demo](https://github.com/torrust/torrust-tracker-demo), which uses this +setup with two floating IPs: + +- HTTP tracker: `http1.torrust-tracker-demo.com` → `116.202.176.169` / `2a01:4f8:1c0c:9aae::1` +- UDP tracker: `udp1.torrust-tracker-demo.com` → `116.202.177.184` / `2a01:4f8:1c0c:828e::1` + +The full incident investigation that led to this documentation is in +[torrust-tracker-demo#2](https://github.com/torrust/torrust-tracker-demo/issues/2). + +## Which Steps Are Needed for Which Scenario + +| Scenario | Step 1 | Step 2 | Step 3 | Step 4 | +| --------------------------------- | ------ | ------ | ------ | ------ | +| Floating IPv4 only | ✅ | — | — | — | +| IPv6 UDP, primary IP only | — | ✅ | ✅ | — | +| IPv6 UDP, floating IP | — | ✅ | ✅ | ✅ | +| Floating IPv4 + IPv6 UDP floating | ✅ | ✅ | ✅ | ✅ | + +--- + +## Why Floating IPs Require Manual Steps + +The deployer configures the tracker to listen on the server's **primary public IP** only. When +traffic arrives on a **Hetzner floating IP**, the kernel's default routing uses the primary IP as +the reply source. The client then receives a reply from a different address than it sent to and +treats it as a timeout (asymmetric routing). + +This applies to **both IPv4 and IPv6** floating IPs. + +--- + +## Step 1 — Floating IP Policy Routing + +> **Required for**: each floating IP (IPv4 or IPv6) + +For each floating IP, add a policy routing rule so that packets arriving on that IP also leave via +that IP. + +On Hetzner, this means adding routing tables (e.g. `100` for IPv4, `200` for IPv6) with a default +route via the floating IP gateway, then adding `ip rule` / `ip -6 rule` entries that match source +addresses on those tables. + +**Persist via netplan** in `/etc/netplan/60-floating-ip.yaml`: + +```yaml +network: + version: 2 + renderer: networkd + ethernets: + eth0: + addresses: + - 116.202.177.184/32 # floating IPv4 (UDP1) + - 2a01:4f8:1c0c:828e::1/64 # floating IPv6 (UDP1) + routing-policy: + - from: 116.202.177.184 + table: 100 + - from: 2a01:4f8:1c0c:828e::1 + table: 200 + routes: + - to: default + via: 172.31.1.1 + table: 100 + - to: default + via: fe80::1 + table: 200 +``` + +Apply: + +```bash +sudo netplan apply +``` + +Verify: + +```bash +ip rule list +ip route show table 100 +ip -6 rule list +ip -6 route show table 200 +``` + +> Repeat for every new floating IP pair. Without this, replies from floating IP endpoints leave +> via the wrong source address. + +--- + +## Step 2 — Enable Docker ip6tables Management + +> **Required for**: IPv6 UDP tracker + +By default, Docker has `ip6tables: false`. This means: + +- Docker does not insert ip6tables rules for published ports (unlike IPv4 where it does this + automatically via iptables). +- Every time Docker starts or restarts a container, it rewrites its own chain tables. This flush + wipes ufw's live ip6tables rules from the kernel. ufw does not automatically reload after this, + so IPv6 UDP traffic is silently dropped after every container restart. + +**Fix**: create `/etc/docker/daemon.json`: + +```json +{ + "ip6tables": true +} +``` + +Apply: + +```bash +sudo systemctl restart docker +``` + +Verify: + +```bash +sudo ip6tables -L ufw6-user-input -n +# Must show: ACCEPT 17 -- ::/0 ::/0 udp dpt:6969 +``` + +--- + +## Step 3 — Enable IPv6 on the Docker Bridge Network + +> **Required for**: IPv6 UDP tracker + +Even with `ip6tables: true`, native IPv6 UDP still fails. Docker spawns `docker-proxy` processes +for each published port. For IPv6, docker-proxy receives packets on an `::` socket but the +container only has an IPv4 address — docker-proxy cannot relay across address families and silently +drops all native IPv6 UDP. + +**Fix**: add `enable_ipv6: true` and a ULA subnet to the bridge network in `docker-compose.yml`: + +```yaml +proxy_network: + driver: bridge + enable_ipv6: true + ipam: + config: + - subnet: "fd01:db8:1::/64" +``` + +With an IPv6 address on the container, Docker creates ip6tables DNAT rules that route native IPv6 +traffic directly to the container, bypassing docker-proxy entirely. + +Apply: + +```bash +cd /opt/torrust +docker compose down +docker compose up -d +``` + +Verify the container has an IPv6 address: + +```bash +docker inspect tracker --format '{{range .NetworkSettings.Networks}}{{.GlobalIPv6Address}} {{end}}' +# Expected: fd01:db8:1::x (non-empty) +``` + +Verify the DNAT rule exists: + +```bash +sudo ip6tables -t nat -L DOCKER -n -v | grep 6969 +# Expected: DNAT rule for dpt:6969 +``` + +--- + +## Step 4 — SNAT for IPv6 UDP Replies via Floating IP + +> **Required for**: floating IPv6 UDP + +After Step 3, the container has a ULA IPv6 address (`fd01:db8:1::x`). When it replies, Docker's +MASQUERADE rule rewrites the source to the server's **primary** IPv6 address +(`2a01:4f8:1c19:620b::1`). Clients that probed the **floating** IPv6 (`2a01:4f8:1c0c:828e::1`) +receive a reply from the wrong address and time out. + +**Fix**: prepend a SNAT rule to `/etc/ufw/before6.rules` **before** the existing `*filter` +section: + +```text +# NAT: rewrite source of Docker UDP tracker IPv6 replies to the floating IP +*nat +:POSTROUTING ACCEPT [0:0] +-A POSTROUTING -s fd01:db8:1::/64 -o eth0 -p udp --sport 6969 \ + -j SNAT --to-source 2a01:4f8:1c0c:828e::1 +COMMIT +``` + +Apply: + +```bash +sudo ufw reload +``` + +Verify: + +```bash +sudo ip6tables -t nat -L POSTROUTING -n -v | grep 6969 +# Expected: SNAT ... fd01:db8:1::/64 ... udp spt:6969 to:2a01:4f8:1c0c:828e::1 +``` + +> This rule must be in `before6.rules` (not added via `ufw` CLI) so it persists in the `*nat` +> table. ufw loads this file at startup, before Docker starts. The SNAT fires before Docker's +> MASQUERADE and takes precedence. +> +> If you change the `subnet` in `docker-compose.yml`, update the `-s` match here too. +> If you add a second floating IPv6, add a second SNAT rule for its subnet/address. + +--- + +## References + +- [torrust/torrust-tracker-demo](https://github.com/torrust/torrust-tracker-demo) — full working configuration +- [torrust-tracker-demo#2](https://github.com/torrust/torrust-tracker-demo/issues/2) — incident that produced this documentation +- [torrust-tracker-demo/docs/docker-ipv6.md](https://github.com/torrust/torrust-tracker-demo/blob/main/docs/docker-ipv6.md) — detailed explanation with packet-flow diagram +- [torrust-tracker-demo/docs/post-deployment.md](https://github.com/torrust/torrust-tracker-demo/blob/main/docs/post-deployment.md) — step-by-step instructions +- [Hetzner Provider Guide](README.md) — Hetzner Cloud configuration for the deployer +- [IPv6 UDP Tracker Issue Investigation](../../../deployments/hetzner-demo-tracker/post-provision/ipv6-udp-tracker-issue.md) — root-cause analysis diff --git a/docs/user-guide/providers/lxd.md b/docs/user-guide/providers/lxd/README.md similarity index 78% rename from docs/user-guide/providers/lxd.md rename to docs/user-guide/providers/lxd/README.md index 1a1bc5c91..18313d2b5 100644 --- a/docs/user-guide/providers/lxd.md +++ b/docs/user-guide/providers/lxd/README.md @@ -14,8 +14,8 @@ LXD provides lightweight virtual machines that run on your local system. Ideal f ## Prerequisites -- LXD installed and initialized (see [LXD tech guide](../../tech-stack/lxd.md)) -- SSH key pair (see [SSH keys guide](../../tech-stack/ssh-keys.md)) +- LXD installed and initialized (see [LXD tech guide](../../../tech-stack/lxd.md)) +- SSH key pair (see [SSH keys guide](../../../tech-stack/ssh-keys.md)) ## LXD-Specific Configuration @@ -73,7 +73,7 @@ sudo systemctl restart snap.lxd.daemon ### Permission Denied -See the [LXD Group Setup](../../tech-stack/lxd.md#proper-lxd-group-setup) section in the LXD tech guide. +See the [LXD Group Setup](../../../tech-stack/lxd.md#proper-lxd-group-setup) section in the LXD tech guide. ### Network Issues @@ -97,7 +97,7 @@ lxc network create lxdbr0 ## SSH Key Behavior -Unlike the [Hetzner provider](hetzner.md), LXD does **not** create a provider-level SSH key resource. This is because: +Unlike the [Hetzner provider](../hetzner/), LXD does **not** create a provider-level SSH key resource. This is because: 1. **Direct console access**: `lxc exec` provides shell access without SSH 2. **No account-level keys**: LXD doesn't have an SSH key registry concept @@ -113,6 +113,6 @@ lxc exec torrust-tracker-vm- -- bash ## Related Documentation -- [LXD Tech Guide](../../tech-stack/lxd.md) - Installation and detailed LXD operations -- [Quick Start: Native](../quick-start/native.md) - LXD deployment workflow -- [Hetzner Provider](hetzner.md) - Cloud deployment alternative +- [LXD Tech Guide](../../../tech-stack/lxd.md) - Installation and detailed LXD operations +- [Quick Start: Native](../../quick-start/native.md) - LXD deployment workflow +- [Hetzner Provider](../hetzner/) - Cloud deployment alternative diff --git a/docs/user-guide/quick-start/docker.md b/docs/user-guide/quick-start/docker.md index 3612aac70..3f2f347f1 100644 --- a/docs/user-guide/quick-start/docker.md +++ b/docs/user-guide/quick-start/docker.md @@ -106,7 +106,7 @@ openssl rand -base64 18 | `grafana.admin_password` | Grafana admin password | (generated secure password) | | `prometheus.scrape_interval_in_secs` | Metrics scrape interval | `15` | -See [Hetzner Provider Guide](../providers/hetzner.md) for all options. +See [Hetzner Provider Guide](../providers/hetzner/) for all options. ## Step 5: Create Environment @@ -334,7 +334,7 @@ This is expected. Docker only supports cloud providers. For LXD, use [Native Ins ## Next Steps -- [Hetzner Provider Guide](../providers/hetzner.md) - Server types, locations, pricing +- [Hetzner Provider Guide](../providers/hetzner/) - Server types, locations, pricing - [Docker Image Reference](../../../docker/deployer/README.md) - Advanced Docker usage - [Command Reference](../commands/README.md) - All available commands diff --git a/docs/user-guide/ssh-keys.md b/docs/user-guide/ssh-keys.md index 7b1882e7f..0602f105c 100644 --- a/docs/user-guide/ssh-keys.md +++ b/docs/user-guide/ssh-keys.md @@ -141,6 +141,6 @@ full `ssh_credentials` field documentation. ## Related Documentation - [Create Environment Command](commands/create.md) — passphrase warning details -- [Hetzner Provider Guide](providers/hetzner.md) — SSH key requirements for cloud deployments +- [Hetzner Provider Guide](providers/hetzner/) — SSH key requirements for cloud deployments - [Security Guide](security.md) — broader security considerations - [ADR: SSH Key Passphrase Detection](../../docs/decisions/ssh-key-passphrase-detection.md) diff --git a/project-words.txt b/project-words.txt index 538c2c162..4f0920930 100644 --- a/project-words.txt +++ b/project-words.txt @@ -523,6 +523,8 @@ usize utmp vbqajnc venv +POSTROUTING +SNAT venvs versionable viewmodel