OCI Network Security Groups Should Not Allow Public RDP
More Info:
Network Security Groups should not allow RDP (port 3389) access from 0.0.0.0/0. Public RDP is one of the most exploited attack vectors for ransomware and unauthorized access.
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
Remediation
Using Console
To stop public RDP access in OCI Network Security Groups (NSGs) using the OCI Console, you need to:
- Find the NSG(s) with an ingress rule allowing TCP/3389 from 0.0.0.0/0 (or ::/0)
- Remove or restrict that rule
- (Optional) Use Monitoring/Alarms to detect if it reappears
Below are the exact steps.
1. Identify NSGs with Public RDP Access
- Sign in to the OCI Console.
- At the top, set the correct Region and Compartment (or choose the parent compartment and enable “Include subcompartments” where applicable).
- In the left menu, go to:
Networking → Network Security Groups. - For each NSG (or those linked to your Windows Compute instances):
- Click the NSG name.
- Go to the Ingress Rules tab.
- Look for rules with:
- Stateless/Stateful: Either
- Source Type: CIDR
- Source CIDR:
0.0.0.0/0(and/or::/0for IPv6) - IP Protocol:
TCP - Destination Port Range:
3389(or a range including 3389)
Any such rule is “public RDP access.”
2. Remediate the NSG Rule (Stop Public RDP)
Option A – Delete the rule (recommended if not required)
- In the NSG Ingress Rules list, find the RDP rule (TCP 3389 from 0.0.0.0/0).
- Click the Actions menu (three dots) next to the rule.
- Click Remove (or Delete rule).
- Confirm deletion.
This immediately blocks public RDP access via that NSG.
Option B – Restrict the rule to specific IPs (if RDP needed)
- In the same Ingress Rules list, click the Actions menu next to the RDP rule.
- Click Edit.
- Change:
- Source Type:
CIDR(orService/NSGif using internal access). - Source CIDR:
- Replace
0.0.0.0/0with your trusted IP/range, e.g.:- Single admin IP:
203.0.113.10/32 - Corporate egress range: e.g.
198.51.100.0/24
- Single admin IP:
- Replace
- Keep IP Protocol as
TCP. - Destination Port Range:
3389(or as needed).
- Source Type:
- Click Save changes.
Now only allowed IPs can reach RDP.
Option C – Move security control from NSG to a bastion / VPN
If you prefer no direct RDP from the internet:
- Remove public RDP rule from the NSG as in Option A.
- Use one of:
- OCI Bastion:
- Go to Identity & Security → Bastion.
- Create a bastion in the same VCN.
- Use Session types like RDP (via SSH tunnel or native where supported).
- Site-to-Site VPN / FastConnect:
- Ensure NSG ingress for RDP only from your on-prem CIDR, not the internet.
- Jump host in a private subnet and only allow SSH/RDP from trusted IPs.
- OCI Bastion:
3. Confirm Instance-Level Exposure
- Go to Compute → Instances.
- Select the Windows instance.
- In the Attached VNICs, note:
- NSG(s) attached to the VNIC.
- Public IP presence:
- If you remove the public IP, RDP is no longer directly exposed to the internet.
- If the instance does not need direct public RDP:
- Under the VNIC, detach or unassign the Public IP, and access through Bastion/VPN instead.
4. (Optional) Add Monitoring / Alerting for Public RDP in Networking
While OCI Monitoring is used primarily for metrics/alarms, for configuration-level findings (like public RDP) you typically use Cloud Guard:
- Go to Identity & Security → Cloud Guard.
- Ensure a Cloud Guard Target is configured for your tenancy/compartments.
- In Detector Recipes, confirm detectors for:
- Public inbound access on sensitive ports (e.g., RDP – TCP/3389).
- Enable Responder Recipes or at least:
- Configure Notifications via Notifications → Topics (e.g., Email/Slack) so any new public RDP rule triggers an alert.
This doesn’t change the NSG by itself unless you enable and configure responders, but it helps detect regressions.
Quick Checklist
- Remove or edit NSG rules allowing TCP 3389 from
0.0.0.0/0or::/0. - Prefer access via OCI Bastion, VPN, or trusted IP/CIDR only.
- Confirm instances do not have unnecessary public IPs.
- Use Cloud Guard to monitor for new public RDP exposures.
If you tell me your current NSG rule details (screenshot or values), I can give you the exact before/after configuration.
Using CLI
Below is a simple, CLI‑only way to:
- Find NSG rules that allow public RDP (TCP/3389 from 0.0.0.0/0), and
- Remove or restrict those rules.
Assumptions:
- You have
ociconfigured. - You know your
compartment-idandvcn-id(if you want to scope by VCN).
1. List NSGs in the compartment (optionally by VCN)
export COMPARTMENT_OCID="<your_compartment_ocid>"
oci network nsg list \
--compartment-id "$COMPARTMENT_OCID" \
--all
Optionally filter to a specific VCN:
export VCN_OCID="<your_vcn_ocid>"
oci network nsg list \
--compartment-id "$COMPARTMENT_OCID" \
--vcn-id "$VCN_OCID" \
--all
Copy the id of each NSG you want to check (e.g., NSG_OCID).
2. Identify NSG rules that allow public RDP
For each NSG:
export NSG_OCID="<your_nsg_ocid>"
oci network nsg get \
--network-security-group-id "$NSG_OCID" \
--query 'data."security-rules"' \
--output table
Look for ingress rules with:
protocol = "6"(TCP),isStateless = falseor true (either is fine),source = "0.0.0.0/0",- and
tcpOptions.destination-port-range.min = 3389andmax = 3389(or a range that includes 3389).
Example offending rule (JSON shape):
{
"description": "Public RDP",
"direction": "INGRESS",
"isStateless": false,
"protocol": "6",
"source": "0.0.0.0/0",
"tcpOptions": {
"destinationPortRange": {
"min": 3389,
"max": 3389
}
}
}
Note the entire rule object; you will need to either remove it or edit it.
3. Export current rules as JSON (for safe editing)
oci network nsg get \
--network-security-group-id "$NSG_OCID" \
--query 'data."security-rules"' \
--output json > nsg-rules.json
Open nsg-rules.json and:
-
Option A – Remove public RDP entirely:
Delete the rule objects that allow TCP/3389 from0.0.0.0/0. -
Option B – Restrict to specific source(s):
Change:"source": "0.0.0.0/0"to a more restrictive CIDR, for example:
"source": "203.0.113.10/32"or your corporate IP range.
Ensure the file remains a valid JSON array of rule objects.
4. Update the NSG with the corrected rules
oci network nsg update \
--network-security-group-id "$NSG_OCID" \
--security-rules file://nsg-rules.json
Verify:
oci network nsg get \
--network-security-group-id "$NSG_OCID" \
--query 'data."security-rules"' \
--output table
Confirm that no INGRESS rule remains with:
source = "0.0.0.0/0",protocol = "6",- and port 3389 (or ranges including 3389).
5. Optional: Scripted detection of public RDP rules
Example using jq to show NSGs with public RDP:
oci network nsg list \
--compartment-id "$COMPARTMENT_OCID" \
--all \
--query 'data[].id' \
--output tsv | while read NSG_ID; do
oci network nsg get \
--network-security-group-id "$NSG_ID" \
--query 'data."security-rules"' \
--output json | \
jq -r --arg nsg "$NSG_ID" '
.[] | select(
.direction=="INGRESS" and
.protocol=="6" and
.source=="0.0.0.0/0" and
(.tcpOptions.destinationPortRange.min <= 3389 and
.tcpOptions.destinationPortRange.max >= 3389)
) | "NSG: \($nsg) has public RDP rule: \(.description // "no description")"
'
done
Use the IDs reported by this script, then fix each NSG as per steps 3–4.
If you want, share a sample nsg-rules.json (with sensitive data removed) and I can show exactly what to edit.
Using Python
Below is a concise, step‑by‑step way to detect and remediate public RDP access in OCI Network Security Groups (NSGs) using Python and the OCI SDK.
1. Prerequisites
- Install the OCI Python SDK:
pip install oci
- Configure your OCI CLI/SDK credentials (
~/.oci/config):[DEFAULT]user=ocid1.user.oc1..xxxxfingerprint=xx:xx:...key_file=/path/to/oci_api_key.pemtenancy=ocid1.tenancy.oc1..xxxxregion=us-ashburn-1
2. Logic You Need
You want to:
- List all NSGs in a compartment (or tenancy).
- For each NSG, list security rules.
- Identify rules that:
- Are ingress,
- Use TCP,
- Allow destination port 3389 (RDP),
- Have source CIDR
0.0.0.0/0or::/0.
- Remove or restrict those rules.
3. Python Script (Detect & Remediate)
This example:
- Scans one compartment.
- Prints the violating rules.
- Optionally removes them (toggle
DRY_RUN).
import oci
# === CONFIG ===
PROFILE = "DEFAULT"
COMPARTMENT_ID = "ocid1.compartment.oc1..xxxx" # root tenancy or specific compartment
DRY_RUN = True # set to False to actually remediate
# RDP details
RDP_PORT = 3389
PUBLIC_CIDRS = ["0.0.0.0/0", "::/0"]
def is_public_rdp_rule(rule):
"""
Check if an NSG security rule is public RDP ingress.
"""
if rule.direction != "INGRESS":
return False
# Only rules with protocol "6" (TCP) or "all"
# In NSG rules protocol is a string: "6" -> TCP, "1" -> ICMP, "17" -> UDP, "all"
if rule.protocol not in ("6", "all"):
return False
# Source must be CIDR block and public
if not rule.source or rule.source not in PUBLIC_CIDRS:
return False
# For protocol "6" (TCP), check destination port range
if rule.protocol == "6" and rule.tcp_options:
dest_port_range = rule.tcp_options.destination_port_range
if dest_port_range:
# Match if rule covers RDP port
if dest_port_range.min <= RDP_PORT <= dest_port_range.max:
return True
return False
# If protocol is "all" with public 0.0.0.0/0, it's even broader
# You may or may not want to treat this as RDP exposure.
# Here we *do* consider it bad for RDP.
if rule.protocol == "all":
return True
return False
def main():
config = oci.config.from_file(profile_name=PROFILE)
network_client = oci.core.VirtualNetworkClient(config)
# 1. List all NSGs in the compartment
nsgs = oci.pagination.list_call_get_all_results(
network_client.list_network_security_groups,
compartment_id=COMPARTMENT_ID
).data
print(f"Found {len(nsgs)} NSGs in compartment {COMPARTMENT_ID}")
for nsg in nsgs:
print(f"\nChecking NSG: {nsg.display_name} ({nsg.id})")
# 2. List rules for each NSG
rules_response = network_client.list_network_security_group_security_rules(
network_security_group_id=nsg.id
)
rules = rules_response.data
bad_rules = [r for r in rules if is_public_rdp_rule(r)]
if not bad_rules:
print(" No public RDP rules found.")
continue
print(f" Found {len(bad_rules)} public RDP rule(s):")
for r in bad_rules:
print(
f" - Rule ID: {r.id}, direction: {r.direction}, "
f"protocol: {r.protocol}, source: {getattr(r, 'source', None)}, "
f"tcp_options: {r.tcp_options}"
)
if DRY_RUN:
print(" DRY_RUN=True -> Not modifying rules.")
continue
# 3. Remediate: remove the bad rules from the NSG
# You must send a *full* new rule set without the bad ones.
new_rules = [r for r in rules if r.id not in {br.id for br in bad_rules}]
# Convert existing rules (which are response models) to UpdateNetworkSecurityGroupSecurityRulesDetails
# We re-use their attributes to preserve non-bad rules.
from oci.core.models import (
UpdateNetworkSecurityGroupSecurityRulesDetails,
AddSecurityRuleDetails
)
update_rules_details = UpdateNetworkSecurityGroupSecurityRulesDetails(
security_rules=[
AddSecurityRuleDetails(
direction=r.direction,
protocol=r.protocol,
description=r.description,
is_stateless=r.is_stateless,
source=getattr(r, "source", None),
source_type=getattr(r, "source_type", 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
)
for r in new_rules
]
)
print(" Updating NSG to remove public RDP rules...")
network_client.update_network_security_group_security_rules(
network_security_group_id=nsg.id,
update_network_security_group_security_rules_details=update_rules_details
)
print(" Update complete.")
print("\nScan/remediation complete.")
if __name__ == "__main__":
main()
4. How to Use This for Monitoring
- Keep
DRY_RUN = Trueand run the script on a schedule (e.g., cron / OCI Functions + Events) to only detect and report. - Send findings to:
- Logging (stdout + OCI Logging),
- Email (OCI Notifications),
- Ticketing system (via webhook/HTTP calls).
5. To Auto‑Remediate
- Set
DRY_RUN = Falsein automation context. - Optionally, instead of removing rules, modify them to a restricted CIDR (e.g., corporate IP range) by adjusting the
AddSecurityRuleDetailsfor matching rules rather than removing them.
Using Terraform
resource "oci_core_network_security_group" "example_nsg" {
# Existing NSG
compartment_id = VAR_COMPARTMENT_OCID # replace with your compartment OCID variable or value
vcn_id = VAR_VCN_OCID # replace with your VCN OCID variable or value
display_name = "example-nsg"
}
# PRIOR MISCONFIGURATION (for reference only, remove from your code if present):
# resource "oci_core_network_security_group_security_rule" "public_rdp" {
# network_security_group_id = oci_core_network_security_group.example_nsg.id
# direction = "INGRESS"
# protocol = "6" # TCP
#
# tcp_options {
# destination_port_range {
# min = 3389
# max = 3389
# }
# }
#
# source = "0.0.0.0/0" # <- PUBLIC, REMOVE/CHANGE THIS
# source_type = "CIDR_BLOCK"
# }
# REMEDIATED RULE: restrict RDP so it is not open to 0.0.0.0/0
resource "oci_core_network_security_group_security_rule" "restricted_rdp" {
network_security_group_id = oci_core_network_security_group.example_nsg.id
direction = "INGRESS"
protocol = "6" # TCP
tcp_options {
destination_port_range {
min = 3389
max = 3389
}
}
# Use a restricted source instead of 0.0.0.0/0.
# Option 1: Limit to a trusted CIDR range
source = "TRUSTED_ADMIN_CIDR" # replace with an allowed admin subnet, e.g. "203.0.113.0/24"
source_type = "CIDR_BLOCK"
# Option 2 (alternative): reference another NSG instead of a CIDR:
# source = oci_core_network_security_group.admin_nsg.id
# source_type = "NETWORK_SECURITY_GROUP"
}
If you are replacing an existing oci_core_network_security_group_security_rule that allowed 0.0.0.0/0 on port 3389, Terraform will show that rule being destroyed and a new, more restrictive rule being created; this is a replacement of the rule resource (not the NSG itself) and will immediately stop public RDP once applied.
For verification, terraform plan should show that any rule with source = "0.0.0.0/0" and destination_port_range 3389 is removed or updated so that:
- no ingress rule on TCP/3389 has
source = "0.0.0.0/0", and - the new/updated rule uses a restricted CIDR or NSG source as in the snippet above.