diff --git a/flake.nix b/flake.nix index 1949cbe..f340cd0 100644 --- a/flake.nix +++ b/flake.nix @@ -275,6 +275,7 @@ specialArgs = { hostname = "hyd-gcp"; secrets = []; + tunnel = false; }; }; migrator-gcp = { diff --git a/hosts/hyd-gcp/configuration.nix b/hosts/hyd-gcp/configuration.nix index 38be2d1..74abbfa 100644 --- a/hosts/hyd-gcp/configuration.nix +++ b/hosts/hyd-gcp/configuration.nix @@ -1,12 +1,14 @@ # one config for every gcp hydrant node, and terraform/nodes passes each node's name in -# as `hostname` +# as `hostname`, and `tunnel` for the one the sitemap worker reads { config, hostname, + lib, pkgs, + tunnel, ... }: { - imports = [./nginx.nix ../../modules/node-secrets.nix]; + imports = [./nginx.nix ../../modules/node-secrets.nix] ++ lib.optional tunnel ./tunnel.nix; networking.hostName = hostname; # the serial console is the only way in if a node fails to boot diff --git a/hosts/hyd-gcp/tunnel.nix b/hosts/hyd-gcp/tunnel.nix new file mode 100644 index 0000000..01ff2bd --- /dev/null +++ b/hosts/hyd-gcp/tunnel.nix @@ -0,0 +1,23 @@ +{hostname, ...}: let + repos = { + proxyPass = "http://127.0.0.1:13010"; + extraConfig = "limit_except GET { deny all; }"; + }; +in { + imports = [../../modules/cloudflared.nix]; + + cloudflared.tokenFile = "/etc/secrets/cloudflared-${hostname}"; + nodeSecrets.readBy."cloudflared-${hostname}" = "cloudflared-${hostname}"; + + services.nginx.virtualHosts.tunnel = { + listen = [ + { + addr = "127.0.0.1"; + port = 8081; + } + ]; + locations."= /repos" = repos; + locations."~ ^/repos/did:[a-z]+:[A-Za-z0-9._%:-]+$" = repos; + locations."/".return = "404"; + }; +} diff --git a/modules/cloudflared.nix b/modules/cloudflared.nix new file mode 100644 index 0000000..b7ab8e4 --- /dev/null +++ b/modules/cloudflared.nix @@ -0,0 +1,50 @@ +{ + config, + lib, + pkgs, + ... +}: let + hostname = config.networking.hostName; +in { + options.cloudflared.tokenFile = lib.mkOption { + type = lib.types.str; + default = "/etc/secrets/cloudflared-${hostname}.token"; + }; + + config.systemd.services."cloudflared-${hostname}" = { + description = "Cloudflare Tunnel connector for ${hostname}"; + wantedBy = ["multi-user.target"]; + wants = ["network-online.target" "hydrant.service"]; + after = ["network-online.target" "hydrant.service"]; + + serviceConfig = { + Type = "simple"; + LoadCredential = ["token:${config.cloudflared.tokenFile}"]; + ExecStart = "${pkgs.cloudflared}/bin/cloudflared tunnel --no-autoupdate --protocol quic run --token-file %d/token"; + Restart = "on-failure"; + RestartSec = "5s"; + DynamicUser = true; + NoNewPrivileges = true; + CapabilityBoundingSet = ""; + DevicePolicy = "closed"; + LockPersonality = true; + MemoryDenyWriteExecute = true; + PrivateDevices = true; + PrivateTmp = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectSystem = "strict"; + RestrictAddressFamilies = ["AF_INET" "AF_INET6"]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + UMask = "0077"; + }; + }; +} diff --git a/modules/hyd-node/cloudflared.nix b/modules/hyd-node/cloudflared.nix index 1221dd8..cf3da31 100644 --- a/modules/hyd-node/cloudflared.nix +++ b/modules/hyd-node/cloudflared.nix @@ -1,12 +1,10 @@ { - config, lib, - pkgs, cloudflareRanges, ... -}: let - hostname = config.networking.hostName; -in { +}: { + imports = [../cloudflared.nix]; + # everything nginx sees arrives through the tunnel, so the client ip has to come from # cloudflare's header services.nginx.commonHttpConfig = '' @@ -14,41 +12,4 @@ in { ") cloudflareRanges} real_ip_header CF-Connecting-IP; ''; - - systemd.services."cloudflared-${hostname}" = { - description = "Cloudflare Tunnel connector for ${hostname}"; - wantedBy = ["multi-user.target"]; - wants = ["network-online.target" "hydrant.service"]; - after = ["network-online.target" "hydrant.service"]; - - serviceConfig = { - Type = "simple"; - LoadCredential = ["token:/etc/secrets/cloudflared-${hostname}.token"]; - ExecStart = "${pkgs.cloudflared}/bin/cloudflared tunnel --no-autoupdate --protocol quic run --token-file %d/token"; - Restart = "on-failure"; - RestartSec = "5s"; - DynamicUser = true; - NoNewPrivileges = true; - CapabilityBoundingSet = ""; - DevicePolicy = "closed"; - LockPersonality = true; - MemoryDenyWriteExecute = true; - PrivateDevices = true; - PrivateTmp = true; - ProtectClock = true; - ProtectControlGroups = true; - ProtectHome = true; - ProtectHostname = true; - ProtectKernelLogs = true; - ProtectKernelModules = true; - ProtectKernelTunables = true; - ProtectSystem = "strict"; - RestrictAddressFamilies = ["AF_INET" "AF_INET6"]; - RestrictNamespaces = true; - RestrictRealtime = true; - RestrictSUIDSGID = true; - SystemCallArchitectures = "native"; - UMask = "0077"; - }; - }; } diff --git a/terraform/README.md b/terraform/README.md index 6de7178..dcaa2dc 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -191,6 +191,11 @@ is that one config with its name passed in, and none of them are in the colmena into `/etc/secrets/` on boot and on any deploy that changes the list. a new version of a secret it already has waits for a reboot or `systemctl restart node-secrets`. - the knot takes git over ssh on 22, so its own ssh is on 2222 (`ssh_port` in `kinds.tf`). +- `tunnel = true` on a hydrant node runs a cloudflare tunnel. this is useful for eg. the + sitemap worker so it can access hydrant's non-public APIs. first make a tunnel without any + public hostname, put its token in the `cloudflared-` secret, and point the worker's + vpc service at `127.0.0.1:8081` (nginx only opens repo reads there). the secret needs its + value before the node deploys with the flag, so apply with `-target` on the secret first. ## workers diff --git a/terraform/modules/envs/outputs.tf b/terraform/modules/envs/outputs.tf index d2870b8..306ed56 100644 --- a/terraform/modules/envs/outputs.tf +++ b/terraform/modules/envs/outputs.tf @@ -48,6 +48,7 @@ locals { machine_type = local.kinds[kind].machine_type disk_gb = local.kinds[kind].disk_gb hostname = null + tunnel = false }, n, { @@ -56,7 +57,7 @@ locals { tag = local.kinds[kind].tag region = hub.cells[n.cell].region public = try(local.kinds[kind].public, null) - secrets = try(local.kinds[kind].secrets, []) + secrets = concat(try(local.kinds[kind].secrets, []), try(n.tunnel, false) ? ["cloudflared-${n.name}"] : []) ssh_port = try(local.kinds[kind].ssh_port, 22) }, ) if !can(n.url)] diff --git a/terraform/modules/nodes/main.tf b/terraform/modules/nodes/main.tf index e237a0a..cb1af41 100644 --- a/terraform/modules/nodes/main.tf +++ b/terraform/modules/nodes/main.tf @@ -234,6 +234,7 @@ module "install" { nixos_partitioner_attr = "${var.flake}#nixosConfigurations.${local.nodes[each.key].system}.config.system.build.diskoScript" special_args = merge( { hostname = each.key, secrets = local.nodes[each.key].secrets }, + local.nodes[each.key].tunnel ? { tunnel = true } : null, # appUrl is for the migrator, the web app it sends users back to local.nodes[each.key].hostname == null ? null : { publicHostname = local.nodes[each.key].hostname