Compute instances are not exposed to the internet
OpenStack compute instances are evaluated to verify they are not exposed to the internet via public IPs (floating IPs or access IPs). Instances with public IPs are directly reachable from the internet, increasing attack surface. Best practices recommend using bastion hosts, VPN gateways, or load balancers instead.
Risk
Instances with public IPs are directly reachable from the internet, enabling reconnaissance, port scanning, and vulnerability exploitation. Attackers can target instances for brute-force attacks, credential stuffing, and malware injection. Public exposure bypasses network segmentation and defense-in-depth. Compromised public instances become pivot points for lateral movement.
Run this check with Prowler
Run in Prowler Cloudprowler openstack --checks compute_instance_public_ip_exposedRecommendation
Avoid public IPs on application/database instances; use private networks only. Deploy bastion hosts or VPN gateways for operator access. Use load balancers (Octavia, HAProxy) with public IPs for web traffic instead of direct instance exposure. Apply least-privilege security groups. Enable flow logs. Use cloud NAT gateways for outbound access without inbound exposure.
Remediation
CLI
openstack server remove floating ip <instance_id> <floating_ip>
ssh -J bastion private-instanceTerraform
# Terraform: deploy instance without public IP (private network only)
resource "openstack_compute_instance_v2" "private_instance" {
name = "private-instance"
image_id = var.image_id
flavor_id = var.flavor_id
key_pair = var.key_pair_name
security_groups = ["internal-sg"]
# Attach to private network ONLY (no floating IP)
network {
name = "private-network"
}
# Do NOT create floating IP association
}
# Bastion host for SSH access (only instance with public IP)
resource "openstack_compute_instance_v2" "bastion" {
name = "bastion-host"
image_id = var.bastion_image_id
flavor_id = "small"
key_pair = var.bastion_keypair
security_groups = ["bastion-sg"] # Restrict SSH to corporate IPs only
network {
name = "dmz-network"
}
}
resource "openstack_networking_floatingip_v2" "bastion_fip" {
pool = "public"
}
resource "openstack_compute_floatingip_associate_v2" "bastion_fip_assoc" {
floating_ip = openstack_networking_floatingip_v2.bastion_fip.address
instance_id = openstack_compute_instance_v2.bastion.id
}
# Security group for bastion (least privilege)
resource "openstack_networking_secgroup_v2" "bastion_sg" {
name = "bastion-sg"
description = "Allow SSH from corporate IPs only"
}
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_sg.id
}Other
Check Code
from typing import List
from prowler.lib.check.models import Check, CheckReportOpenStack
from prowler.providers.openstack.services.compute.compute_client import compute_client
from prowler.providers.openstack.services.compute.lib.ip import is_public_ip
class compute_instance_public_ip_exposed(Check):
"""Ensure compute instances are not exposed to the internet via public IP addresses."""
def execute(self) -> List[CheckReportOpenStack]:
findings: List[CheckReportOpenStack] = []
for instance in compute_client.instances:
report = CheckReportOpenStack(metadata=self.metadata(), resource=instance)
# Collect all potential public IP indicators
public_ips = []
# Check SDK computed properties
if instance.public_v4:
public_ips.append(f"Public IPv4: {instance.public_v4}")
if instance.public_v6:
public_ips.append(f"Public IPv6: {instance.public_v6}")
if instance.access_ipv4:
public_ips.append(f"Access IPv4: {instance.access_ipv4}")
if instance.access_ipv6:
public_ips.append(f"Access IPv6: {instance.access_ipv6}")
# Check networks for any additional public IPs (beyond first one captured in SDK attributes)
# This handles cases where instances have multiple public IPs on different networks
sdk_ips = {
instance.public_v4,
instance.public_v6,
instance.access_ipv4,
instance.access_ipv6,
}
for network_name, ip_list in instance.networks.items():
for ip in ip_list:
# Check if IP is public and not already captured in SDK attributes
if is_public_ip(ip) and ip not in sdk_ips:
public_ips.append(
f"{network_name} network: {ip} (public range)"
)
# Remove duplicates while preserving order
seen = set()
unique_public_ips = []
for ip in public_ips:
if ip not in seen:
seen.add(ip)
unique_public_ips.append(ip)
if not unique_public_ips:
report.status = "PASS"
report.status_extended = f"Instance {instance.name} ({instance.id}) is not exposed to the internet (no public IP addresses or external network attachments detected)."
else:
report.status = "FAIL"
ip_list = ", ".join(unique_public_ips)
report.status_extended = f"Instance {instance.name} ({instance.id}) is exposed to the internet with public IP addresses: {ip_list}."
findings.append(report)
return findings