diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-20 12:27:24 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-20 12:27:24 +0200 |
| commit | b5271e79dfca05e9745b66c3b8b096ee21a833c3 (patch) | |
| tree | 39b6893cc403c1d94a1fd44314d411624356084d /snippets/hyperstack | |
| parent | 79797aea15b44272fd099ea0611a74497b3200ca (diff) | |
task 297: lock down default ingress rules
Diffstat (limited to 'snippets/hyperstack')
| -rw-r--r-- | snippets/hyperstack/README.md | 7 | ||||
| -rw-r--r-- | snippets/hyperstack/hyperstack-vm.toml | 6 | ||||
| -rw-r--r-- | snippets/hyperstack/hyperstack-vm1.toml | 6 | ||||
| -rw-r--r-- | snippets/hyperstack/hyperstack-vm2.toml | 6 | ||||
| -rwxr-xr-x | snippets/hyperstack/hyperstack.rb | 85 |
5 files changed, 98 insertions, 12 deletions
diff --git a/snippets/hyperstack/README.md b/snippets/hyperstack/README.md index 9e196a8..6175d61 100644 --- a/snippets/hyperstack/README.md +++ b/snippets/hyperstack/README.md @@ -26,6 +26,9 @@ The VM gets `192.168.3.1`; your local machine gets `192.168.3.2`. - Hyperstack account with API key in `~/.hyperstack` - SSH key registered in Hyperstack as `earth` (or change `ssh.hyperstack_key_name` in the TOML) +- Review `[network].allowed_ssh_cidrs` and `[network].allowed_wireguard_cidrs` in your TOML. + The secure default is `["auto"]`, which resolves your current public egress IP to `/32`. + Set explicit CIDRs or `HYPERSTACK_OPERATOR_CIDR` if you deploy from a different network. - WireGuard setup script: `wg1-setup.sh` (present in this directory) - Ruby with `toml-rb` gem: `bundle install` @@ -124,6 +127,10 @@ Edit `hyperstack-vm.toml` to change defaults. Key sections: | `[network]` | Ports, WireGuard subnet, allowed CIDRs | | `[wireguard]` | Auto-setup script path | +`allowed_ssh_cidrs` and `allowed_wireguard_cidrs` accept either explicit CIDRs such as +`["203.0.113.4/32"]` or `["auto"]`. `auto` resolves the current public operator IP at runtime; +set `HYPERSTACK_OPERATOR_CIDR` to override that detection when needed. + ## Monitoring vLLM ```bash diff --git a/snippets/hyperstack/hyperstack-vm.toml b/snippets/hyperstack/hyperstack-vm.toml index e23294f..e82c97f 100644 --- a/snippets/hyperstack/hyperstack-vm.toml +++ b/snippets/hyperstack/hyperstack-vm.toml @@ -31,12 +31,14 @@ connect_timeout_sec = 10 [network] wireguard_udp_port = 56710 wireguard_subnet = "192.168.3.0/24" +# Secure default: "auto" resolves your current public egress IP to /32 at runtime. +# Override with explicit CIDRs if you deploy from multiple networks or want broader access. +allowed_ssh_cidrs = ["auto"] +allowed_wireguard_cidrs = ["auto"] # Port 11434 is shared by both Ollama and vLLM for firewall compatibility. ollama_port = 11434 # Port 4000: LiteLLM Anthropic-API proxy (used with vLLM). litellm_port = 4000 -allowed_ssh_cidrs = ["0.0.0.0/0"] -allowed_wireguard_cidrs = ["0.0.0.0/0"] [bootstrap] enable_guest_bootstrap = true diff --git a/snippets/hyperstack/hyperstack-vm1.toml b/snippets/hyperstack/hyperstack-vm1.toml index c5c940a..1b116bd 100644 --- a/snippets/hyperstack/hyperstack-vm1.toml +++ b/snippets/hyperstack/hyperstack-vm1.toml @@ -35,12 +35,14 @@ wireguard_subnet = "192.168.3.0/24" # VM1 gets the first server-side WireGuard IP (gateway address + 0). # earth (client) is 192.168.3.2; VM1 is 192.168.3.1; VM2 is 192.168.3.3. wireguard_server_ip = "192.168.3.1" +# Secure default: "auto" resolves your current public egress IP to /32 at runtime. +# Override with explicit CIDRs if you deploy from multiple networks or want broader access. +allowed_ssh_cidrs = ["auto"] +allowed_wireguard_cidrs = ["auto"] # Port 11434 is shared by both Ollama and vLLM for firewall compatibility. ollama_port = 11434 # Port 4000: LiteLLM Anthropic-API proxy (used with vLLM). litellm_port = 4000 -allowed_ssh_cidrs = ["0.0.0.0/0"] -allowed_wireguard_cidrs = ["0.0.0.0/0"] [bootstrap] enable_guest_bootstrap = true diff --git a/snippets/hyperstack/hyperstack-vm2.toml b/snippets/hyperstack/hyperstack-vm2.toml index 6cc6503..e8e9b00 100644 --- a/snippets/hyperstack/hyperstack-vm2.toml +++ b/snippets/hyperstack/hyperstack-vm2.toml @@ -35,12 +35,14 @@ wireguard_subnet = "192.168.3.0/24" # VM2 gets the third server-side WireGuard IP (skipping .2 which is the earth client). # earth (client) is 192.168.3.2; VM1 is 192.168.3.1; VM2 is 192.168.3.3. wireguard_server_ip = "192.168.3.3" +# Secure default: "auto" resolves your current public egress IP to /32 at runtime. +# Override with explicit CIDRs if you deploy from multiple networks or want broader access. +allowed_ssh_cidrs = ["auto"] +allowed_wireguard_cidrs = ["auto"] # Port 11434 is shared by both Ollama and vLLM for firewall compatibility. ollama_port = 11434 # Port 4000: LiteLLM Anthropic-API proxy (used with vLLM). litellm_port = 4000 -allowed_ssh_cidrs = ["0.0.0.0/0"] -allowed_wireguard_cidrs = ["0.0.0.0/0"] [bootstrap] enable_guest_bootstrap = true diff --git a/snippets/hyperstack/hyperstack.rb b/snippets/hyperstack/hyperstack.rb index 9f3d289..a11c2d3 100755 --- a/snippets/hyperstack/hyperstack.rb +++ b/snippets/hyperstack/hyperstack.rb @@ -87,8 +87,8 @@ module HyperstackVM 'wireguard_server_ip' => nil, 'ollama_port' => 11_434, 'litellm_port' => 4_000, - 'allowed_ssh_cidrs' => ['0.0.0.0/0'], - 'allowed_wireguard_cidrs' => ['0.0.0.0/0'] + 'allowed_ssh_cidrs' => ['auto'], + 'allowed_wireguard_cidrs' => ['auto'] }, 'bootstrap' => { 'enable_guest_bootstrap' => true, @@ -152,8 +152,15 @@ module HyperstackVM raise Error, "Missing [ssh].#{key} in config #{path}" if blank?(dig('ssh', key)) end - [fetch('network', 'wireguard_subnet'), *fetch('network', 'allowed_ssh_cidrs'), - *fetch('network', 'allowed_wireguard_cidrs')].each do |cidr| + ssh_cidrs = normalized_cidrs(fetch('network', 'allowed_ssh_cidrs')) + wireguard_cidrs = normalized_cidrs(fetch('network', 'allowed_wireguard_cidrs')) + + raise Error, missing_cidr_message('allowed_ssh_cidrs') if ssh_cidrs.empty? + raise Error, missing_cidr_message('allowed_wireguard_cidrs') if wireguard_cidrs.empty? + + [fetch('network', 'wireguard_subnet'), *ssh_cidrs, *wireguard_cidrs].each do |cidr| + next if cidr == 'auto' + IPAddr.new(cidr) rescue IPAddr::InvalidAddressError => e raise Error, "Invalid CIDR #{cidr.inspect}: #{e.message}" @@ -192,6 +199,14 @@ module HyperstackVM value == true end + def normalized_cidrs(values) + Array(values).map { |value| value.to_s.strip }.reject(&:empty?) + end + + def missing_cidr_message(key) + "Missing [network].#{key} in config #{path}; set it to one or more CIDRs, or ['auto'] to restrict access to the current public operator IP." + end + def deep_merge(left, right) left.merge(right) do |_key, old_value, new_value| if old_value.is_a?(Hash) && new_value.is_a?(Hash) @@ -344,11 +359,11 @@ module HyperstackVM end def allowed_ssh_cidrs - Array(fetch('network', 'allowed_ssh_cidrs')).map(&:to_s) + resolved_allowed_cidrs('allowed_ssh_cidrs') end def allowed_wireguard_cidrs - Array(fetch('network', 'allowed_wireguard_cidrs')).map(&:to_s) + resolved_allowed_cidrs('allowed_wireguard_cidrs') end def guest_bootstrap_enabled? @@ -534,6 +549,62 @@ module HyperstackVM value == true end + def resolved_allowed_cidrs(key) + values = Array(fetch('network', key)).map { |value| value.to_s.strip }.reject(&:empty?) + values.flat_map { |value| value == 'auto' ? [detected_operator_cidr] : [value] }.uniq + end + + def detected_operator_cidr + return @detected_operator_cidr if defined?(@detected_operator_cidr) + + configured = ENV['HYPERSTACK_OPERATOR_CIDR'].to_s.strip + @detected_operator_cidr = normalize_operator_cidr(configured) unless configured.empty? + return @detected_operator_cidr if defined?(@detected_operator_cidr) + + @detected_operator_cidr = detect_public_operator_cidr + end + + def normalize_operator_cidr(value) + ip = IPAddr.new(value) + suffix = ip.ipv4? ? 32 : 128 + value.include?('/') ? value : "#{ip}/#{suffix}" + rescue IPAddr::InvalidAddressError => e + raise Error, "Invalid HYPERSTACK_OPERATOR_CIDR #{value.inspect}: #{e.message}" + end + + def detect_public_operator_cidr + [ + 'https://api.ipify.org', + 'https://ifconfig.me/ip', + 'https://ipv4.icanhazip.com' + ].each do |url| + cidr = fetch_public_cidr(url) + return cidr if cidr + end + + source = path || 'the active config' + raise Error, + "Unable to detect the current public operator IP for [network].allowed_*_cidrs = ['auto']. Set HYPERSTACK_OPERATOR_CIDR or replace 'auto' with explicit CIDRs in #{source}." + end + + def fetch_public_cidr(url) + uri = URI(url) + response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', open_timeout: 5, read_timeout: 5) do |http| + http.request(Net::HTTP::Get.new(uri)) + end + return nil unless response.is_a?(Net::HTTPSuccess) + + body = response.body.to_s.strip + return nil if body.empty? + + ip = IPAddr.new(body) + suffix = ip.ipv4? ? 32 : 128 + "#{ip}/#{suffix}" + rescue IPAddr::InvalidAddressError, SocketError, SystemCallError, Timeout::Error, Net::OpenTimeout, + Net::ReadTimeout, OpenSSL::SSL::SSLError + nil + end + def custom_user_data inline = dig('vm', 'user_data') return inline unless inline.nil? || inline.empty? @@ -1569,6 +1640,8 @@ module HyperstackVM info "Resolved image: #{resolved[:image]['name']}" info "Resolved SSH keypair: #{resolved[:keypair]['name']}" info "Planned VM name: #{vm_name}" + info "Allowed SSH CIDRs: #{@config.allowed_ssh_cidrs.join(', ')}" + info "Allowed WireGuard CIDRs: #{@config.allowed_wireguard_cidrs.join(', ')}" info 'Create payload:' @out.puts(JSON.pretty_generate(payload)) if @config.guest_bootstrap_enabled? |
