> ## Documentation Index
> Fetch the complete documentation index at: https://cloudanix.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# OCI Network Security Groups Should Not Allow Public SSH Access

### More Info:

Network Security Groups should not allow SSH (port 22) access from 0.0.0.0/0. Public SSH endpoints are continuously targeted by automated brute-force attacks.

### Risk Level

Critical

### Address

Compliance, Security

### Compliance Standards

* APRA CPS 234 (Australia)
* BSI C5 (Germany)
* Brazil LGPD
* CCPA / CPRA (California)
* CIS Critical Security Controls v8
* CMMC 2.0
* CSA Cloud Controls Matrix v4
* DPDPA
* Digital Operational Resilience Act (EU)
* ISO/IEC 27017
* ISO/IEC 27018
* ISO/IEC 27701
* KSA PDPL
* MAS Technology Risk Management (Singapore)
* MITRE ATT\&CK (Cloud)
* NIS2 Directive
* NIST
* NIST SP 800-171
* NYDFS 23 NYCRR 500
* SWIFT Customer Security Controls Framework
* Sarbanes-Oxley IT General Controls
* Securities and Exchange Board of India (SEBI) - Cloud Security Adoption Framework
* UK NCSC Cyber Assessment Framework

### Triage and Remediation

<Tabs>
  <Tab title="Remediation">
    ### Remediation

    <AccordionGroup>
      <Accordion title="Using Console" defaultOpen="true">
        To remediate “OCI Network Security Groups Should Not Allow Public SSH Access” using the OCI Console:

        1. **Identify the offending NSG rule**
           * In the OCI Console, open the navigation menu.
           * Go to **Logging & Monitoring → Security → Cloud Guard** (or **Security Zones** / your monitoring tool) and open the **detector** or **finding** that reports:
             * *“Network Security Groups Should Not Allow Public SSH Access”* (or similar).
           * From the finding details, note:
             * The **Compartment**
             * The **VCN**
             * The **Network Security Group (NSG)** name or OCID
           * Click the NSG link if available, or navigate manually (next steps).

        2. **Navigate to the Network Security Group**
           * Open the navigation menu.
           * Go to **Networking → Virtual Cloud Networks**.
           * Select the **compartment** where the VCN/NSG resides.
           * Click the **VCN** that contains the NSG.
           * In the VCN details page, under **Resources**, click **Network Security Groups**.
           * Click the **NSG** identified in the finding.

        3. **Review ingress (inbound) rules**
           * In the NSG details page, click the **Ingress Rules** tab.
           * Look for any rule that:
             * Uses **Destination Port Range** = `22` (or `22-22`)
             * AND **Source Type** = `CIDR`
             * AND **Source CIDR** is:
               * `0.0.0.0/0` (IPv4 public)
               * or `::/0` (IPv6 public)
             * AND protocol is **TCP** (or **All protocols**).

        4. **Decide remediation approach**
           You have two secure options:
           * **Restrict SSH to trusted IP ranges** (e.g., your office VPN or bastion host):
             * Replace `0.0.0.0/0` with a specific CIDR, e.g. `203.0.113.10/32`.
           * **Remove direct SSH entirely** (recommended when using **OCI Bastion**, Session Manager, or other jump hosts):
             * Delete the rule that allows SSH from the internet.

        5. **Edit or delete the problematic rule**
           * On the **Ingress Rules** tab:
             * To **delete**:
               * Click the **menu (⋯)** or **Delete** icon next to the SSH rule.
               * Confirm deletion.
             * To **edit** (if you need SSH but only from specific sources):
               * Click **Edit** (if available) or delete and recreate:
                 * If editing:
                   * Change **Source Type** to `CIDR`.
                   * Set **Source CIDR** to your **trusted IP/CIDR** (e.g. `198.51.100.0/24`).
                   * Ensure **IP Protocol** is `TCP`.
                   * Ensure **Destination Port Range** is `22` (or a custom SSH port if you changed it).
                   * Click **Save Changes**.
                 * If recreating:
                   * Click **Add Ingress Rule**.
                   * Set:
                     * **Source Type**: `CIDR`
                     * **Source CIDR**: your secure range (e.g., VPN range)
                     * **IP Protocol**: `TCP`
                     * **Destination Port Range**: `22`
                     * Optionally set **Stateless** = `No` (default).
                   * Click **Add Ingress Rule**.

        6. **Check for IPv6 exposure (if applicable)**
           * Still on **Ingress Rules**, confirm there is no rule with:
             * **Source CIDR** = `::/0`
             * Protocol `TCP`
             * Destination port `22`.
           * If present, remove or restrict it the same way as for IPv4.

        7. **Validate remediation in Monitoring/Cloud Guard**
           * Go back to **Cloud Guard → Detector Findings** (or your monitoring view).
           * Locate the original finding.
           * After a short interval (detection schedule), the finding should:
             * Move to **Resolved** (or similar status),
             * Or disappear from the active list.
           * If it still persists, confirm again that:
             * No NSG ingress rule allows port 22 from `0.0.0.0/0` or `::/0`.
             * No “All protocols / All ports” rule is using `0.0.0.0/0` or `::/0` for the same NSG.

        8. **(Optional) Add safer access method**
           * If you removed public SSH, consider:
             * **OCI Bastion**:
               * Navigation: **Identity & Security → Bastion**.
               * Create a bastion in the same VCN and subnet, then use it for SSH sessions.
             * Or a VPN/Private connectivity plus NSG rules that only allow SSH from that private network.

        If you share the specific rule format (screenshot or text), I can give you the exact field values to use when editing it.
      </Accordion>

      <Accordion title="Using CLI">
        Below are concise, step‑by‑step OCI CLI instructions to **find and fix NSG rules that allow public SSH (TCP/22 from 0.0.0.0/0)**.

        Assumptions:

        * You have OCI CLI installed and configured.
        * You know the `compartment-ocid` (and optionally `vcn-id`).

        ***

        ## 1. List Network Security Groups (NSGs)

        ```bash theme={null}
        oci network nsg list \
          --compartment-id <compartment-ocid> \
          --all
        ```

        Note the `id` of the NSG(s) you want to check.

        Optionally filter by VCN:

        ```bash theme={null}
        oci network nsg list \
          --compartment-id <compartment-ocid> \
          --vcn-id <vcn-ocid> \
          --all
        ```

        ***

        ## 2. List Security Rules in Each NSG

        For each NSG:

        ```bash theme={null}
        oci network nsg rules list \
          --network-security-group-id <nsg-ocid> \
          --all \
          --output table
        ```

        You’re looking for **ingress** rules with:

        * `direction = INGRESS`
        * `protocol = 6` (TCP)
        * `source = 0.0.0.0/0` (or `::/0` for IPv6)
        * `tcpOptions.destinationPortRange.min = 22` and `.max = 22` (or range covering 22)

        Example JSON view:

        ```bash theme={null}
        oci network nsg rules list \
          --network-security-group-id <nsg-ocid> \
          --all \
          --output json
        ```

        ***

        ## 3. Remove or Restrict the Public SSH Rule

        You have two main options:

        ### Option A – Delete the offending rule

        You must know the rule defined in the NSG (i.e., its `isStateless`, `protocol`, `source`, `tcpOptions`, etc.), because OCI NSG rules are updated by **replacing the full rule set**.

        1. Get current rules into a file:

        ```bash theme={null}
        oci network nsg rules list \
          --network-security-group-id <nsg-ocid> \
          --all \
          --output json > current_rules.json
        ```

        2. Edit `current_rules.json`:

           * Remove any rule object that:
             * is `direction: "INGRESS"`
             * and matches `protocol: "6"` with TCP port 22
             * and has `source: "0.0.0.0/0"` (or `::/0`).

           Keep only the safe rules. Save as `updated_rules.json`.

           The JSON to send to `update` must be just the array of rules, e.g.:

           ```json theme={null}
           {
             "securityRules": [
               {
                 "direction": "INGRESS",
                 "isStateless": false,
                 "protocol": "6",
                 "source": "10.0.0.0/16",
                 "sourceType": "CIDR_BLOCK",
                 "tcpOptions": {
                   "destinationPortRange": {
                     "min": 443,
                     "max": 443
                   }
                 }
               }
               // ...other safe rules...
             ]
           }
           ```

        3. Apply the new rule set:

        ```bash theme={null}
        oci network nsg rules update \
          --network-security-group-id <nsg-ocid> \
          --security-rules file://updated_rules.json
        ```

        This removes the public SSH rule from that NSG.

        ***

        ### Option B – Restrict SSH to a specific source (e.g., your office IP range)

        Instead of deleting, you can adjust the `source` from `0.0.0.0/0` to a trusted CIDR, like `203.0.113.0/24`.

        1. Same as above, export rules:

        ```bash theme={null}
        oci network nsg rules list \
          --network-security-group-id <nsg-ocid> \
          --all \
          --output json > current_rules.json
        ```

        2. In `current_rules.json`:

           * Find the rule with:
             * `direction: "INGRESS"`
             * `protocol: "6"`
             * `source: "0.0.0.0/0"`
             * TCP port 22
           * Change `source` to your allowed CIDR, for example:

           ```json theme={null}
           {
             "direction": "INGRESS",
             "isStateless": false,
             "protocol": "6",
             "source": "203.0.113.0/24",
             "sourceType": "CIDR_BLOCK",
             "tcpOptions": {
               "destinationPortRange": {
                 "min": 22,
                 "max": 22
               }
             }
           }
           ```

           Save as `updated_rules.json` in the same wrapper structure:

           ```json theme={null}
           {
             "securityRules": [
               // ...modified and other rules...
             ]
           }
           ```

        3. Apply:

        ```bash theme={null}
        oci network nsg rules update \
          --network-security-group-id <nsg-ocid> \
          --security-rules file://updated_rules.json
        ```

        ***

        ## 4. Verify That Public SSH Is Blocked

        Re-list NSG rules and confirm there is **no** ingress rule with `source` `0.0.0.0/0` (or `::/0`) and TCP destination port 22:

        ```bash theme={null}
        oci network nsg rules list \
          --network-security-group-id <nsg-ocid> \
          --all \
          --output table
        ```

        Optionally use `jq`:

        ```bash theme={null}
        oci network nsg rules list \
          --network-security-group-id <nsg-ocid> \
          --all \
          --output json | \
        jq '.data[] | select(.direction=="INGRESS" and .protocol=="6" and .source=="0.0.0.0/0" and .tcp-options.destination-port-range.min<=22 and .tcp-options.destination-port-range.max>=22)'
        ```

        If that command returns nothing, your NSG no longer allows public SSH.

        ***

        If you share a sample `oci network nsg rules list` JSON output, I can craft the exact `updated_rules.json` you should apply.
      </Accordion>

      <Accordion title="Using Python">
        Below is a practical way to detect and remediate “public SSH” from OCI Network Security Groups using Python and the OCI SDK.

        Assumptions:

        * You already have:
          * OCI Python SDK installed: `pip install oci`
          * Config file at `~/.oci/config` (or equivalent) with a profile (e.g. `DEFAULT`)
        * You want to:
          * Find NSG rules that allow SSH (TCP/22) from `0.0.0.0/0`
          * Remove or adjust them programmatically

        ***

        ## 1. Decide your remediation strategy

        You have two main options:

        1. **Delete the offending rule(s)**\
           – safest default if you don’t know the intended source.

        2. **Restrict CIDR instead of deleting** (e.g. change `0.0.0.0/0` to `x.y.z.0/24`)\
           – safer operationally if you know the correct IP ranges.

        The script below shows **both**, with a toggle.

        ***

        ## 2. Python script: identify & remediate NSG rules

        ```python theme={null}
        import oci
        from oci.core import VirtualNetworkClient
        from oci.core.models import (
            UpdateNetworkSecurityGroupSecurityRulesDetails,
            UpdateSecurityRuleDetails,
            TcpOptions
        )

        # ========= USER CONFIGURATION =========
        PROFILE_NAME = "DEFAULT"         # oci config profile
        COMPARTMENT_OCID = "<your_compartment_ocid>"  # or leave None to scan entire tenancy (with recursion)
        DRY_RUN = True                   # True = only report; False = actually remediate
        RESTRICT_INSTEAD_OF_DELETE = False  # True = modify CIDR instead of deleting rule
        NEW_ALLOWED_CIDR = "203.0.113.0/24"  # used only if RESTRICT_INSTEAD_OF_DELETE
        # =====================================

        def is_ssh_rule_from_anywhere(rule):
            """
            Returns True if:
              - INGRESS rule
              - protocol TCP (6)
              - source 0.0.0.0/0
              - destination port range includes 22
            """
            if rule.direction != "INGRESS":
                return False

            # protocol '6' is TCP, 'all' = 'all' or 'all' numeric, but we care about explicit '6'
            if rule.protocol not in ("6", "all"):
                return False

            if getattr(rule, "source", None) != "0.0.0.0/0":
                return False

            # ssh is TCP/22
            tcp_opts = getattr(rule, "tcp_options", None)
            if tcp_opts is None:
                # For "all ports", this also counts as exposing SSH
                return True

            dest_range = tcp_opts.destination_port_range
            if dest_range is None:
                # Also "all ports" within TCP
                return True

            # Check if 22 is within the defined port range
            if dest_range.min <= 22 <= dest_range.max:
                return True

            return False


        def main():
            config = oci.config.from_file("~/.oci/config", PROFILE_NAME)
            identity_client = oci.identity.IdentityClient(config)
            vcn_client = VirtualNetworkClient(config)

            tenancy_id = config["tenancy"]

            # If COMPARTMENT_OCID is None, enumerate all compartments in tenancy
            compartments_to_scan = []
            if COMPARTMENT_OCID:
                compartments_to_scan.append(COMPARTMENT_OCID)
            else:
                # recursive list of all compartments
                response = oci.pagination.list_call_get_all_results(
                    identity_client.list_compartments,
                    tenancy_id,
                    compartment_id_in_subtree=True
                )
                compartments_to_scan = [c.id for c in response.data] + [tenancy_id]

            print(f"Scanning compartments: {compartments_to_scan}")

            for compartment_id in compartments_to_scan:
                nsgs = oci.pagination.list_call_get_all_results(
                    vcn_client.list_network_security_groups,
                    compartment_id=compartment_id
                ).data

                for nsg in nsgs:
                    nsg_id = nsg.id
                    nsg_name = nsg.display_name

                    rules_resp = vcn_client.list_network_security_group_security_rules(nsg_id)
                    rules = rules_resp.data

                    offending_rules = [r for r in rules if is_ssh_rule_from_anywhere(r)]

                    if not offending_rules:
                        continue

                    print(f"\n[FOUND] NSG: {nsg_name} ({nsg_id}) has public SSH rules:")
                    for r in offending_rules:
                        print(f"  - Rule ID: {r.id}, protocol: {r.protocol}, source: {r.source}, direction: {r.direction}")

                    if DRY_RUN:
                        print("  DRY_RUN=True => not changing anything.")
                        continue

                    # Build new rules list: either remove or modify the offending ones
                    new_rules = []
                    for r in rules:
                        if r in offending_rules:
                            if RESTRICT_INSTEAD_OF_DELETE:
                                # Modify source CIDR only; keep the rest
                                print(f"  Modifying rule {r.id} source from {r.source} to {NEW_ALLOWED_CIDR}")
                                updated = UpdateSecurityRuleDetails(
                                    id=r.id,
                                    direction=r.direction,
                                    protocol=r.protocol,
                                    description=r.description,
                                    is_stateless=r.is_stateless,
                                    source_type=getattr(r, "source_type", None),
                                    source=NEW_ALLOWED_CIDR,
                                    destination=r.destination if hasattr(r, "destination") else None,
                                    destination_type=getattr(r, "destination_type", None),
                                    tcp_options=r.tcp_options,
                                    udp_options=r.udp_options,
                                    icmp_options=r.icmp_options
                                )
                                new_rules.append(updated)
                            else:
                                # Deleting the rule (skip adding it)
                                print(f"  Deleting rule {r.id} that allows SSH from 0.0.0.0/0")
                            continue

                        # Keep non-offending rules as-is
                        updated = UpdateSecurityRuleDetails(
                            id=r.id,
                            direction=r.direction,
                            protocol=r.protocol,
                            description=r.description,
                            is_stateless=r.is_stateless,
                            source_type=getattr(r, "source_type", None),
                            source=getattr(r, "source", None),
                            destination=getattr(r, "destination", None),
                            destination_type=getattr(r, "destination_type", None),
                            tcp_options=r.tcp_options,
                            udp_options=r.udp_options,
                            icmp_options=r.icmp_options
                        )
                        new_rules.append(updated)

                    # Push the updated rules back to the NSG (this replaces all existing rules)
                    update_details = UpdateNetworkSecurityGroupSecurityRulesDetails(
                        security_rules=new_rules
                    )
                    print(f"  Applying update to NSG {nsg_name} ({nsg_id})...")
                    vcn_client.update_network_security_group_security_rules(
                        network_security_group_id=nsg_id,
                        update_network_security_group_security_rules_details=update_details
                    )
                    print("  Update complete.")

            print("\nScan/remediation finished.")

        if __name__ == "__main__":
            main()
        ```

        ***

        ## 3. How to use this in a monitoring/remediation workflow

        1. **Monitoring-only mode**
           * Set `DRY_RUN = True` to run this script on a schedule (e.g., via OCI Functions + Events or an external scheduler) as a *monitor* and just log/report policy violations.

        2. **Auto-remediation mode**
           * Set `DRY_RUN = False`
           * Choose:
             * `RESTRICT_INSTEAD_OF_DELETE = False` to delete insecure rules, or
             * `RESTRICT_INSTEAD_OF_DELETE = True` and set `NEW_ALLOWED_CIDR` to your admin or bastion IP range.

        3. Integrate with **Cloud Guard** or other monitoring:
           * Use Cloud Guard to generate findings for “NSG with public SSH”.
           * Trigger this script via OCI Functions or an external automation pipeline when such findings appear.

        If you tell me how you’re currently doing “OCI Networking Monitoring” (Cloud Guard, custom scripts, Functions, etc.), I can adapt this to plug directly into that workflow.
      </Accordion>

      <Accordion title="Using Terraform">
        ```hcl theme={null}
        resource "oci_core_network_security_group" "example" {
          # Replace with your NSG details
          compartment_id = VAR_COMPARTMENT_OCID   # substitute your compartment OCID
          vcn_id         = VAR_VCN_OCID           # substitute your VCN OCID
          display_name   = "example-nsg"
        }

        # Ingress SSH rule restricted to a non-public CIDR instead of 0.0.0.0/0
        resource "oci_core_network_security_group_security_rule" "ssh_restricted" {
          network_security_group_id = oci_core_network_security_group.example.id

          description = "Restricted SSH access (no public 0.0.0.0/0)"

          direction = "INGRESS"
          protocol  = "6" # TCP

          source_type = "CIDR_BLOCK"
          source      = "ALLOWED_SSH_CIDR" # substitute your allowed CIDR, e.g. "10.0.0.0/16" or a jump host /32

          tcp_options {
            destination_port_range {
              min = 22
              max = 22
            }
          }
        }
        ```

        If you currently have an `oci_core_network_security_group_security_rule` for SSH with `source = "0.0.0.0/0"`, change its `source` to a non-public CIDR (as above) or remove that rule resource entirely; changing the rule will force replacement of the security rule itself but not the NSG.

        For verification, `terraform plan` should show the NSG security rule with `source` changing from `"0.0.0.0/0"` to your restricted CIDR, or the public SSH rule being destroyed with no replacement.
      </Accordion>
    </AccordionGroup>
  </Tab>
</Tabs>
