3838ADD_SECURITY_RULES_MAX_CHUNK_SIZE = 25
3939LIST_OBJECTS_MAX_LIMIT = 1000
4040VCN_CIDR = "10.0.0.0/16"
41- # CIDR for the dedicated VCN used for custom-NSG instances (see `get_or_create_restricted_vcn`).
42- # Must not overlap `VCN_CIDR` in case the two VCNs are ever peered.
43- RESTRICTED_VCN_CIDR = "10.1.0.0/16"
4441WAIT_FOR_COMPARTMENT_ATTEMPS = 36
4542WAIT_FOR_COMPARTMENT_DELAY = 5
4643
@@ -559,75 +556,52 @@ def get_or_create_vcn(
559556def get_or_create_subnet (
560557 name : str , vcn_id : str , compartment_id : str , client : oci .core .VirtualNetworkClient
561558) -> oci .core .models .Subnet :
562- query_results = chain_paginated_responses (
563- client .list_subnets , compartment_id = compartment_id , display_name = name
564- )
565- if subnet := next (query_results , None ):
566- return subnet
567-
568- return client .create_subnet (
569- oci .core .models .CreateSubnetDetails (
570- cidr_block = VCN_CIDR ,
571- compartment_id = compartment_id ,
572- display_name = name ,
573- vcn_id = vcn_id ,
574- )
575- ).data
576-
577-
578- def get_or_create_restricted_vcn (
579- name : str , compartment_id : str , client : oci .core .VirtualNetworkClient
580- ) -> oci .core .models .Vcn :
581559 """
582- Like `get_or_create_vcn`, but a separate VCN (own CIDR block) dedicated to
583- custom-NSG instances. A *separate* VCN is used - rather than a second subnet
584- in the existing default VCN - because the default subnet already occupies
585- the default VCN's entire CIDR block, and adding a second CIDR block to an
586- existing VCN requires an async OCI operation (`add_vcn_cidr`) that takes
587- the VCN out of service for subnet/route-table updates for its duration.
588- A brand new VCN avoids that entirely and keeps the default VCN/subnet used
589- by dstack's auto-managed instances completely untouched.
590- """
591- query_results = chain_paginated_responses (
592- client .list_vcns , compartment_id = compartment_id , display_name = name
593- )
594- if vcn := next (query_results , None ):
595- return vcn
596-
597- return client .create_vcn (
598- oci .core .models .CreateVcnDetails (
599- cidr_blocks = [RESTRICTED_VCN_CIDR ],
600- compartment_id = compartment_id ,
601- display_name = name ,
602- )
603- ).data
604-
605-
606- def get_or_create_restricted_subnet (
607- name : str , vcn_id : str , compartment_id : str , client : oci .core .VirtualNetworkClient
608- ) -> oci .core .models .Subnet :
609- """
610- Like `get_or_create_subnet`, but creates the subnet with an empty list of
611- security lists (`security_list_ids=[]`) instead of letting OCI attach the
612- VCN's permissive default security list. Must be created in a VCN returned
613- by `get_or_create_restricted_vcn`, not the default VCN.
614-
615- This is used for instances that run with a user-managed (custom) network
616- security group. With no security list contributing rules, the NSG becomes
617- the sole source of truth for what traffic is allowed to and from these
618- instances. OCI evaluates security lists and NSGs as a union of allows, so an
619- empty security list list simply means "the security-list layer grants
620- nothing"; it does not deny anything on its own.
560+ The subnet is created with an empty list of security lists
561+ (`security_list_ids=[]`) instead of letting OCI attach the VCN's permissive
562+ default security list. All instances - whether they use dstack's
563+ auto-managed network security group (NSG) or a user-supplied custom one -
564+ live in this single shared subnet, so the NSG attached to each instance's
565+ VNIC is the sole security boundary; there is no separate security-list
566+ layer to reason about.
567+
568+ A single shared subnet (rather than a second, NSG-only subnet) is required
569+ because OCI NSGs are scoped to a single VCN: an NSG can only be attached to
570+ a VNIC whose subnet belongs to the *same* VCN the NSG was created in. Since
571+ dstack only manages one VCN, a user's custom NSG must live in - and a
572+ custom-NSG instance must therefore be placed in a subnet within - that same
573+ VCN.
574+
575+ For instances using dstack's auto-managed NSG, the rules normally
576+ contributed by the security list (SSH ingress, unrestricted egress) are
577+ added directly to that NSG instead; see
578+ `update_security_group_rules_for_runner_instances`. Instances using a
579+ user-supplied NSG get no such compensating rules - per dstack's "fully
580+ hands-off" contract, it never adds, removes, or otherwise modifies rules on
581+ a user-supplied security group, so the user is fully responsible for
582+ allowing the traffic their instances need (including SSH).
621583 """
622584 query_results = chain_paginated_responses (
623585 client .list_subnets , compartment_id = compartment_id , display_name = name
624586 )
625587 if subnet := next (query_results , None ):
588+ if subnet .security_list_ids :
589+ # A subnet created before this fix still has the VCN's default
590+ # security list attached. Since dstack owns this subnet
591+ # (it is not user-supplied), it's safe to update it in place -
592+ # detaching the security list so the NSG becomes the sole
593+ # security boundary for every instance in it, matching newly
594+ # created subnets. This is unrelated to dstack's "fully
595+ # hands-off" contract for user-supplied *security groups*, which
596+ # this does not touch.
597+ subnet = client .update_subnet (
598+ subnet .id , oci .core .models .UpdateSubnetDetails (security_list_ids = [])
599+ ).data
626600 return subnet
627601
628602 return client .create_subnet (
629603 oci .core .models .CreateSubnetDetails (
630- cidr_block = RESTRICTED_VCN_CIDR ,
604+ cidr_block = VCN_CIDR ,
631605 compartment_id = compartment_id ,
632606 display_name = name ,
633607 vcn_id = vcn_id ,
@@ -636,28 +610,6 @@ def get_or_create_restricted_subnet(
636610 ).data
637611
638612
639- def set_up_restricted_network_resources_in_region (
640- compartment_id : str , project_name : str , client : oci .core .VirtualNetworkClient
641- ) -> oci .core .models .Subnet :
642- """
643- Like `set_up_network_resources_in_region`, but for the dedicated VCN/subnet
644- used by custom-NSG instances (see `get_or_create_restricted_vcn` and
645- `get_or_create_restricted_subnet`). Idempotent - safe to call on every
646- instance launch, mirroring how `get_or_create_security_group` is already
647- called on every launch for the default (non-custom-NSG) path.
648- """
649- vcn = get_or_create_restricted_vcn (
650- f"dstack-{ project_name } -restricted-vcn" , compartment_id , client
651- )
652- internet_gateway = get_or_create_internet_gateway (
653- f"dstack-{ project_name } -restricted-internet-gateway" , vcn .id , compartment_id , client
654- )
655- update_route_table (vcn .default_route_table_id , internet_gateway .id , client )
656- return get_or_create_restricted_subnet (
657- f"dstack-{ project_name } -restricted-subnet" , vcn .id , compartment_id , client
658- )
659-
660-
661613def get_or_create_internet_gateway (
662614 name : str , vcn_id : str , compartment_id : str , client : oci .core .VirtualNetworkClient
663615) -> oci .core .models .InternetGateway :
@@ -714,8 +666,11 @@ def get_or_create_security_group(
714666def update_security_group_rules_for_runner_instances (
715667 security_group_id : str , client : oci .core .VirtualNetworkClient
716668) -> None :
717- # These rules are combined with subnet's default Security List that allows
718- # ingress TCP on port 22 from anywhere
669+ # The subnet these instances live in has no security list attached (see
670+ # `get_or_create_subnet`), so this NSG must grant everything a runner
671+ # instance needs on its own: SSH ingress from anywhere and unrestricted
672+ # egress, in addition to allowing all traffic between instances that share
673+ # this NSG.
719674 rules = [
720675 SecurityRule (
721676 description = "Allow all traffic within this security group" ,
@@ -724,6 +679,23 @@ def update_security_group_rules_for_runner_instances(
724679 source = security_group_id ,
725680 protocol = "all" ,
726681 ),
682+ SecurityRule (
683+ description = "Allow SSH ingress from anywhere" ,
684+ direction = oci .core .models .AddSecurityRuleDetails .DIRECTION_INGRESS ,
685+ source_type = oci .core .models .AddSecurityRuleDetails .SOURCE_TYPE_CIDR_BLOCK ,
686+ source = "0.0.0.0/0" ,
687+ protocol = "6" , # TCP
688+ tcp_options = oci .core .models .TcpOptions (
689+ destination_port_range = oci .core .models .PortRange (min = 22 , max = 22 )
690+ ),
691+ ),
692+ SecurityRule (
693+ description = "Allow all egress traffic" ,
694+ direction = oci .core .models .AddSecurityRuleDetails .DIRECTION_EGRESS ,
695+ destination_type = oci .core .models .AddSecurityRuleDetails .DESTINATION_TYPE_CIDR_BLOCK ,
696+ destination = "0.0.0.0/0" ,
697+ protocol = "all" ,
698+ ),
727699 ]
728700 update_security_group_rules (security_group_id , rules , client )
729701
0 commit comments