Skip to main content

OCI Network Security Groups Should Not Allow Public SSH

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

Remediation

Using Console

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.

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)

oci network nsg list \
--compartment-id <compartment-ocid> \
--all

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

Optionally filter by VCN:

oci network nsg list \
--compartment-id <compartment-ocid> \
--vcn-id <vcn-ocid> \
--all

2. List Security Rules in Each NSG

For each NSG:

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:

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:
oci network nsg rules list \
--network-security-group-id <nsg-ocid> \
--all \
--output json > current_rules.json
  1. 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.:

    {
    "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...
    ]
    }
  2. Apply the new rule set:

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:
oci network nsg rules list \
--network-security-group-id <nsg-ocid> \
--all \
--output json > current_rules.json
  1. 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:
    {
    "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:

    {
    "securityRules": [
    // ...modified and other rules...
    ]
    }
  2. Apply:

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:

oci network nsg rules list \
--network-security-group-id <nsg-ocid> \
--all \
--output table

Optionally use jq:

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.

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

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.

Using Terraform
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.