Security groups do not allow SSH from the Internet
OpenStack security groups are evaluated to verify that SSH (port 22) is not exposed to the Internet (0.0.0.0/0 or ::/0). Security groups act as virtual firewalls controlling instance traffic. Unrestricted SSH access violates least privilege and creates significant attack surface. Best practices recommend restricting SSH to known IP ranges, bastion hosts, or VPN gateways.
Risk
Unrestricted SSH exposes instances to brute-force attacks, password testing, and SSH vulnerability exploitation (CVE-2023-38408, CVE-2021-41617) for initial access. Compromised instances enable persistence, privilege escalation, lateral movement, crypto mining, DDoS botnets, data exfiltration, and ransomware deployment. SSH exposure bypasses defense-in-depth and enables direct production access.
Run this check with Prowler
Run in Prowler Cloudprowler openstack --checks networking_security_group_allows_ssh_from_internetRecommendation
Restrict SSH access to known IP ranges using bastion hosts or VPN gateways instead of allowing 0.0.0.0/0. Implement multi-factor authentication, disable password authentication, and use SSH certificates for centralized key management. Monitor SSH logs for failed login attempts and enforce fail2ban or similar IP blocking mechanisms.
Remediation
Terraform
# Terraform: restrict SSH to known IP ranges, never 0.0.0.0/0
resource "openstack_networking_secgroup_v2" "app_servers" {
name = "app-servers-sg"
description = "Security group for application servers"
}
# GOOD: SSH restricted to corporate office IP
resource "openstack_networking_secgroup_rule_v2" "ssh_from_office" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 22
port_range_max = 22
remote_ip_prefix = "203.0.113.0/24" # Corporate office network
security_group_id = openstack_networking_secgroup_v2.app_servers.id
}
# GOOD: SSH from bastion security group (reference-based rule)
resource "openstack_networking_secgroup_rule_v2" "ssh_from_bastion" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 22
port_range_max = 22
remote_group_id = openstack_networking_secgroup_v2.bastion.id # Reference, not CIDR
security_group_id = openstack_networking_secgroup_v2.app_servers.id
}
# Bastion security group (least privilege)
resource "openstack_networking_secgroup_v2" "bastion" {
name = "bastion-sg"
description = "Security group for bastion host - only entry point for SSH"
}
resource "openstack_networking_secgroup_rule_v2" "bastion_ssh" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 22
port_range_max = 22
remote_ip_prefix = var.corporate_cidr # e.g., 203.0.113.0/24
security_group_id = openstack_networking_secgroup_v2.bastion.id
}
# BAD: Never do this - SSH from anywhere (0.0.0.0/0)
# resource "openstack_networking_secgroup_rule_v2" "ssh_from_anywhere" {
# direction = "ingress"
# ethertype = "IPv4"
# protocol = "tcp"
# port_range_min = 22
# port_range_max = 22
# remote_ip_prefix = "0.0.0.0/0" # DANGEROUS - DO NOT USE
# security_group_id = openstack_networking_secgroup_v2.app_servers.id
# }Other
Check Code
from typing import List
from prowler.lib.check.models import Check, CheckReportOpenStack
from prowler.providers.openstack.lib.security_groups import check_security_group_rule
from prowler.providers.openstack.services.networking.networking_client import (
networking_client,
)
class networking_security_group_allows_ssh_from_internet(Check):
"""Ensure security groups do not allow SSH from the Internet."""
def execute(self) -> List[CheckReportOpenStack]:
findings: List[CheckReportOpenStack] = []
for sg in networking_client.security_groups:
report = CheckReportOpenStack(metadata=self.metadata(), resource=sg)
report.resource_id = sg.id
report.resource_name = sg.name
report.region = sg.region
# Check if any rule allows SSH from 0.0.0.0/0 or ::/0
ssh_exposed = False
exposed_rules = []
for rule in sg.security_group_rules:
if check_security_group_rule(
rule=rule,
protocol="tcp",
ports=[22],
any_address=True,
direction="ingress",
):
ssh_exposed = True
cidr = rule.remote_ip_prefix or "0.0.0.0/0"
exposed_rules.append(
f"rule {rule.id} ({rule.protocol}/{cidr}:{rule.port_range_min}-{rule.port_range_max})"
)
if ssh_exposed:
report.status = "FAIL"
rules_str = ", ".join(exposed_rules)
report.status_extended = f"Security group {sg.name} ({sg.id}) allows unrestricted SSH access (port 22) from the Internet via {rules_str}."
else:
report.status = "PASS"
report.status_extended = f"Security group {sg.name} ({sg.id}) does not allow SSH (port 22) from the Internet."
findings.append(report)
return findings