Feat: custom security groups#4027
Conversation
… GCP, OCI backends Adds a way to point dstack at a pre-existing, user-managed network security resource instead of the one dstack creates and manages automatically (which always opens SSH to 0.0.0.0/0). Useful for private-network setups (e.g. a VPC only reachable via a Tailscale subnet router). - AWS: security_group_id (backend config) - Azure: network_security_group (backend config) - OCI: network_security_group_id (backend config) - GCP: create_firewall_rules: false (backend config; GCP firewall rules are VPC-wide, not an attachable per-instance resource, so there is no fleet-level override for GCP) - AWS/Azure/OCI also support a fleet/run-level override via the security_group profile property (mirrors the existing reservation field), which takes precedence over the backend config default. When a custom security group is configured, dstack attaches it as-is and never adds, removes, or modifies rules on it. Users are responsible for SSH reachability and, for multi-node clusters, for allowing traffic between instances in the group. Gateway security groups/NSGs (AWS, Azure, OCI) are unaffected by this change and keep dstack's existing auto-managed behavior.
security_group_id / network_security_group / network_security_group_id were flat single-value fields, but security groups and NSGs are scoped per VPC/region, so they only worked for single-region backends. Fixed to mirror the existing vpc_ids/subnet_ids/subnet_ids_per_region conventions: - AWS: security_group_name (a name that must exist in every region's VPC — AWS allows reusing the same group name across regions) and security_group_ids (an explicit region -> ID map for when names differ). - Azure: network_security_group_ids (location -> NSG name map). Azure NSG names are unique per resource group regardless of location, so a single name can never cover more than one region. - OCI: network_security_group_ids (region -> NSG OCID map). Regions/locations not covered by the mapping fall back to dstack's auto-created security group, so partial custom-SG adoption across regions works too. GCP is unaffected (its firewall rules are VPC-wide, not per-region).
An independent review of the custom-security-group feature (previous two commits) found several real gaps. Fixed all of them: - security_group was dropped when a run provisioned a new instance into an existing fleet (only the fleet-apply path honored it). Fixed by threading security_group through the Requirements pipeline exactly like reservation already is (Requirements.security_group, combine_fleet_and_run_profiles/ combine_fleet_and_run_requirements, and sourcing run_job's InstanceConfiguration from job.job_spec.requirements.security_group instead of the run's raw profile). - security_group was silently ignored when an offer resolved to a backend that doesn't support it (e.g. GCP). offers.py now narrows backend_types to BACKENDS_WITH_SECURITY_GROUP_SUPPORT when security_group is set, mirroring the existing reservation filtering. - AWS: the configurator forbade combining security_group_name with security_group_ids, but compute.py implements (and docs/tests described) a fallback from ids to name - the combination is now allowed. Also added validation catching region-key typos in security_group_ids, and a clearer ComputeError instead of a confusing NoCapacityError retry loop when a configured security group doesn't exist in the target VPC. - Azure: renamed network_security_group_ids to network_security_group_names since the values are NSG names (not IDs) scoped to the backend's resource_group, and added region-key typo validation. - GCP: create_firewall_rules no longer disables the gateway firewall rule, matching AWS/Azure/OCI where gateway security resources are always auto-managed regardless of the custom-security-group settings. - OCI: the shared subnet has no security_list_ids, so it inherits the VCN's permissive default security list (SSH open to 0.0.0.0/0, allow-all egress). Since OCI evaluates security lists and NSGs as a union of allows, a custom NSG could not actually restrict anything. Fixed by routing custom-NSG instances into a separate, dedicated VCN/subnet with no security list, so the NSG becomes the sole security boundary. The default VCN/subnet used by auto-managed-NSG instances is completely untouched. Also added region-key typo validation and corrected the docs to accurately attribute default SSH exposure to the security list, not the NSG.
- run_job(): source reservation/security_group from the `requirements` parameter (already fleet+run-combined) instead of job.job_spec.requirements (run-only). Without this, a run provisioning new capacity into a fleet with a fleet-level security_group/reservation would silently ignore it - the same latent bug reservation already had, inherited by security_group. - Azure create_gateway(): use get_gateway_network_security_group_name (the dedicated, always-created gateway NSG) instead of get_default_network_security_group_name. The default/per-location instance NSG can now be skipped when network_security_group_names covers that location, which would have broken gateway provisioning since it was referencing an NSG that might not exist. - OCI: redesign custom-NSG networking. The previous "separate restricted VCN" approach is fundamentally broken - OCI network security groups are VCN-scoped, so a user's NSG can never be attached to an instance in a different VCN than the one the NSG lives in. Fixed by using a single shared subnet for all instances (default-NSG and custom-NSG alike), with no OCI security list attached. dstack's auto-managed NSG now carries explicit SSH ingress and all-egress rules to compensate for the removed security list; custom NSGs remain fully hands-off, per the feature's contract. Existing subnets are migrated in place (security list detached) since the subnet is dstack-owned infrastructure, not a user-supplied resource. Updated docs to clarify a custom NSG must live in dstack's own default VCN. Regression: 388 core backend tests + base/azure/oci targeted suites + server routers/services (requirements, offers, fleets, runs, backends) all passing. Pre-existing unrelated failures (verda/vastai/nebius modules not installed in this environment) left untouched.
| By default, `dstack` creates and manages its own security group per project (opening SSH to `0.0.0.0/0` | ||
| and allowing all traffic within the group so multi-node clusters work out of the box). |
There was a problem hiding this comment.
Please try to use the same wording across backends for consistency (with backend-specific terminology) . Something like this:
By default, dstack creates and manages its own security group per project that allows: SSH ingress (TCP port 22) from 0.0.0.0/0, unrestricted egress, and all traffic within the group so that multi-node clusters work out of the box.
|
@james-boydell, by default I understand that security groups are complimentary to "no public ip" and can be useful to limit the connections from entire VPCs/peered VPCs to specific IPs, control the blast radius, meet the compliance etc. So PR makes sense 100%. Can you expand why custom security groups are needed for your particular use case? |
Custom security groups / NSGs / firewall rules
Adds support for using a pre-existing security group instead of the one dstack creates automatically, so instances can run in private VPCs without an internet-exposed rule.
New security_group profile property (project-level default via backend config, overridable per fleet/run).
Supported on AWS (security_group_name/security_group_ids), Azure (network_security_group_names), and OCI (network_security_group_ids), each with per-region/location mapping. GCP gets a create_firewall_rules: false opt-out instead, since GCP firewall rules are VPC-wide.
When a custom security group is set, dstack never adds, removes, or modifies its rules — you're fully responsible for ingress/egress.
Docs updated in mkdocs/docs/concepts/backends.md.