Skip to content

Feat: custom security groups#4027

Draft
james-boydell wants to merge 5 commits into
dstackai:masterfrom
james-boydell:feat/custom-security-groups
Draft

Feat: custom security groups#4027
james-boydell wants to merge 5 commits into
dstackai:masterfrom
james-boydell:feat/custom-security-groups

Conversation

@james-boydell

Copy link
Copy Markdown
Contributor

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.

… 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.
Comment on lines +246 to +247
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).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@r4victor

Copy link
Copy Markdown
Collaborator

@james-boydell, by default dstack indeed creates security groups / firewalls that allows SSH ingress from any IP. The public access can be disabled if you provision instances without public IPs – so security groups are not necessary to limit the public access – just to be clear.

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants