From 3e202ddcf872fc982598d8823c93e1cd79bcb1b6 Mon Sep 17 00:00:00 2001 From: Wojciech Kocjan Date: Thu, 17 Sep 2026 11:54:48 +0200 Subject: [PATCH 1/2] docs(azure): reserve NSG priorities 3000 to 3999 Ringleader writes its inbound admission rule at the lowest free NSG priority from 3000 to 3999. Both Azure routes now ask you to leave that range free, in place of 4090 to 4096. --- azure/README.md | 2 +- azure/arm/README.md | 2 +- azure/arm/azuredeploy-network.json | 4 ++-- azure/terraform/main.tf | 2 +- azure/terraform/variables.tf | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/azure/README.md b/azure/README.md index 084ab04..c0c8bcb 100644 --- a/azure/README.md +++ b/azure/README.md @@ -248,7 +248,7 @@ first policy: decide on their own, which **widens** inbound. Without one, it admits only TCP 22 and 2222 from outside the VNet. Move those rules onto the subnet NSG first. Ringleader adds a rule there, but never edits or deletes one you wrote. -- **Leave priorities 4090 to 4096 free in the subnet NSG.** Ringleader adds one inbound allow rule +- **Leave priorities 3000 to 3999 free in the subnet NSG.** Ringleader adds one inbound allow rule of its own there, at the lowest free priority in that range, so its workstations are reachable without you widening the group. The rule admits TCP 22 and 2222 from any address. Its destination is an application security group that Ringleader creates in your resource group and diff --git a/azure/arm/README.md b/azure/arm/README.md index f16318d..5a21887 100644 --- a/azure/arm/README.md +++ b/azure/arm/README.md @@ -134,7 +134,7 @@ and 2222 from outside the VNet. Keep inbound narrowing here rather than on a NIC outbound `Deny` here: it cannot tighten a policy and it can break one. See [`../README.md`](../README.md#two-nsgs-at-two-layers--and-which-one-is-yours). -**Leave priorities 4090 to 4096 free in both groups.** In the workstations group, Ringleader adds +**Leave priorities 3000 to 3999 free in both groups.** In the workstations group, Ringleader adds one inbound allow rule in that range, admitting the SSH ports to the workstations it creates. In the gateway group it will add one admitting the management ports to the gateway VM. Ringleader never edits or deletes a rule this template created. Every rule here is deployed as its own diff --git a/azure/arm/azuredeploy-network.json b/azure/arm/azuredeploy-network.json index 4f76124..8c008e8 100644 --- a/azure/arm/azuredeploy-network.json +++ b/azure/arm/azuredeploy-network.json @@ -118,7 +118,7 @@ "type": "Bool", "defaultValue": false, "metadata": { - "description": "False, the default, creates the workstations network security group. True leaves the group that is already there alone and deploys only its rules. Deploying a network security group sets its whole rule list, so redeploying a group that already exists deletes every rule added since, including the inbound rule Ringleader adds to admit its own workstations at a priority between 4090 and 4096. deploy.sh looks the group up and passes this for you. Each rule below is its own child resource either way, so changing sshSourceCidr and re-running still lands." + "description": "False, the default, creates the workstations network security group. True leaves the group that is already there alone and deploys only its rules. Deploying a network security group sets its whole rule list, so redeploying a group that already exists deletes every rule added since, including the inbound rule Ringleader adds to admit its own workstations at a priority between 3000 and 3999. deploy.sh looks the group up and passes this for you. Each rule below is its own child resource either way, so changing sshSourceCidr and re-running still lands." } }, "gatewayNsgExists": { @@ -251,7 +251,7 @@ } }, { - "comments": "The workstations subnet's security group, and the governed subnet's. Its rules are child resources below rather than a securityRules list here, because a list is deployed as a WHOLE: setting it deletes every rule this template does not name, and Ringleader writes one of its own in this group to admit its workstations, at a priority between 4090 and 4096. Leave that range free. A child resource adds and updates only itself, so re-running this template leaves Ringleader's rule in place -- as long as the group itself is not redeployed, which is what workstationsNsgExists is for.", + "comments": "The workstations subnet's security group, and the governed subnet's. Its rules are child resources below rather than a securityRules list here, because a list is deployed as a WHOLE: setting it deletes every rule this template does not name, and Ringleader writes one of its own in this group to admit its workstations, at a priority between 3000 and 3999. Leave that range free. A child resource adds and updates only itself, so re-running this template leaves Ringleader's rule in place -- as long as the group itself is not redeployed, which is what workstationsNsgExists is for.", "condition": "[not(parameters('workstationsNsgExists'))]", "type": "Microsoft.Network/networkSecurityGroups", "apiVersion": "2023-11-01", diff --git a/azure/terraform/main.tf b/azure/terraform/main.tf index 386a053..f8cd544 100644 --- a/azure/terraform/main.tf +++ b/azure/terraform/main.tf @@ -363,7 +363,7 @@ resource "azurerm_subnet_network_security_group_association" "gateway" { # replaced group was narrowing anything. For a box without a policy it admits only TCP 22 and 2222 # from outside the VNet, so that box takes no other port from there, whatever this group opens. # Ringleader adds one inbound rule of its own in this group, at the lowest free priority -# between 4090 and 4096: TCP 22 and 2222 from any address, to an application security group holding +# between 3000 and 3999: TCP 22 and 2222 from any address, to an application security group holding # its workstations' NICs. It never edits or deletes a rule declared here, and each rule below is its # own resource, so an apply leaves Ringleader's in place and Ringleader leaves these in place. # diff --git a/azure/terraform/variables.tf b/azure/terraform/variables.tf index 3b5fa39..edbd4ca 100644 --- a/azure/terraform/variables.tf +++ b/azure/terraform/variables.tf @@ -364,7 +364,7 @@ variable "ssh_source_ranges" { Ringleader has no bastion and no SSH tunnel: `rl shell`, `rl tmux`, port-forwards and VS Code Web all dial the workstation on 22. Ringleader adds its own rule to this NSG, at a - priority between 4090 and 4096, admitting TCP 22 and 2222 from any address to the + priority between 3000 and 3999, admitting TCP 22 and 2222 from any address to the workstations it creates and to no other VM. List CIDRs here to reach the other VMs on those subnets, or to keep your engineers able to reach a workstation when Ringleader cannot write its rule. Such a workstation reports SSHAdmissionMissing. From b933d15c1c77c988f4f62b5cba5341b4555a1290 Mon Sep 17 00:00:00 2001 From: Wojciech Kocjan Date: Thu, 17 Sep 2026 12:31:52 +0200 Subject: [PATCH 2/2] docs(azure): reserve the range in the gateway NSG too --- azure/README.md | 6 ++++++ azure/arm/README.md | 8 ++++---- azure/arm/azuredeploy-network.json | 2 +- azure/terraform/main.tf | 6 ++++++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/azure/README.md b/azure/README.md index c0c8bcb..ff18cbf 100644 --- a/azure/README.md +++ b/azure/README.md @@ -327,6 +327,12 @@ workstation's own NSG still decides whether to accept it. To close the rule, set (`deploy.sh`). A steered workstation is then reachable only from inside the VNet or a network joined to it. +**Leave priorities 3000 to 3999 free in this NSG too.** By default, Ringleader adds one inbound +allow rule of its own here, at the lowest free priority in that range. The rule admits TCP from +any address on the ports the gateway forwards to steered workstations, a range that differs from +`allow-management-inbound`'s. Its destination is an application security group holding only +Ringleader's gateway VMs. Ringleader never edits or deletes a rule you wrote. + **Hand its id back as `spec.subnet` on the `EgressGateway`.** It is `gateway_subnet_id` in the handoff, and Ringleader builds no gateway VM until it has one: a route table attaches per subnet and replaces the default route of everything in it, so a proxy sitting in a subnet it diff --git a/azure/arm/README.md b/azure/arm/README.md index 5a21887..8c439ce 100644 --- a/azure/arm/README.md +++ b/azure/arm/README.md @@ -136,10 +136,10 @@ outbound `Deny` here: it cannot tighten a policy and it can break one. See **Leave priorities 3000 to 3999 free in both groups.** In the workstations group, Ringleader adds one inbound allow rule in that range, admitting the SSH ports to the workstations it creates. In -the gateway group it will add one admitting the management ports to the gateway VM. Ringleader -never edits or deletes a rule this template created. Every rule here is deployed as its own -`securityRules` child resource, and `deploy.sh` passes `workstationsNsgExists` and -`gatewayNsgExists`, for one reason: deploying a group sets its **whole** rule list. A group +the gateway group it adds one by default, admitting the ports the gateway forwards to steered +workstations. Ringleader never edits or deletes a rule this template created. Every rule here is +deployed as its own `securityRules` child resource, and `deploy.sh` passes `workstationsNsgExists` +and `gatewayNsgExists`, for one reason: deploying a group sets its **whole** rule list. A group redeployed over one that already exists would delete Ringleader's rule and leave the workstations behind it unreachable until Ringleader's next pass. The other side of that shape is that a deployment never deletes a rule, so clearing `SSH_SOURCE_CIDR`, `SECONDARY_SSH_SOURCE_CIDR` or diff --git a/azure/arm/azuredeploy-network.json b/azure/arm/azuredeploy-network.json index 8c008e8..fae1713 100644 --- a/azure/arm/azuredeploy-network.json +++ b/azure/arm/azuredeploy-network.json @@ -298,7 +298,7 @@ } }, { - "comments": "The egress gateway subnet's own security group. Azure's default rules are rules INSIDE a group, and a Standard public IP, which the gateway VM carries whenever EgressGateway.spec.publicAddress asks for one, is closed to inbound traffic unless an NSG allows it. This group holds those allows, so the gateway VM takes at most what the rules below admit. Its rules are child resources for the reason the workstations group's are: Ringleader will add a rule of its own here too, admitting the management band to the gateway VM, and a securityRules list here would delete it on every re-run.", + "comments": "The egress gateway subnet's own security group. Azure's default rules are rules INSIDE a group, and a Standard public IP, which the gateway VM carries whenever EgressGateway.spec.publicAddress asks for one, is closed to inbound traffic unless an NSG allows it. This group holds those allows, so the gateway VM takes at most what the rules below admit. Its rules are child resources for the reason the workstations group's are: Ringleader adds a rule of its own here too by default, admitting the ports the gateway forwards to steered workstations, and a securityRules list here would delete it on every re-run.", "condition": "[and(parameters('createGatewaySubnet'), not(parameters('gatewayNsgExists')))]", "type": "Microsoft.Network/networkSecurityGroups", "apiVersion": "2023-11-01", diff --git a/azure/terraform/main.tf b/azure/terraform/main.tf index f8cd544..7fc7e5d 100644 --- a/azure/terraform/main.tf +++ b/azure/terraform/main.tf @@ -244,6 +244,12 @@ resource "azurerm_subnet" "workstations" { # ports from the ranges you named, and from nowhere else". Outbound is untouched, so the gateway # keeps the internet access it exists to police (AllowInternetOutBound at 65001). # +# By default, Ringleader adds one inbound rule of its own in this group, at the lowest free priority +# between 3000 and 3999: TCP from any address on the ports the gateway forwards to steered +# workstations, to an application security group holding its gateway VMs. It never edits or deletes +# a rule declared here, and each rule below is its own resource, so an apply leaves Ringleader's in +# place and Ringleader leaves these in place. +# # The subnet is associated with the NAT gateway below, so anything placed here has egress without an # address of its own. resource "azurerm_subnet" "gateway" {