Compute instance metadata does not contain sensitive data
OpenStack compute instance metadata is evaluated to detect sensitive data such as passwords, API keys, secrets, and private keys. Instance metadata is accessible via the metadata service (169.254.169.254) to any process inside the instance. Storing secrets in metadata exposes them to SSRF attacks, compromised applications, and unauthorized access.
Risk
Instance metadata containing sensitive data exposes credentials through the metadata service (169.254.169.254), accessible to any process inside the instance. Attackers exploiting SSRF, compromised applications, or insider threats can extract passwords, API keys, and private keys. Stolen credentials enable unauthorized modifications, privilege escalation, resource deletion, and cryptomining.
Run this check with Prowler
Run in Prowler Cloudprowler openstack --checks compute_instance_metadata_sensitive_dataRecommendation
Never store secrets in metadata; use Barbican (OpenStack Key Manager), Vault, or external secrets management instead. Retrieve secrets at runtime via APIs. Implement least privilege access to secrets with RBAC. Enable secrets audit logging. Use envelope encryption for secrets at rest. Implement automatic rotation every 90 days. Scan metadata for hardcoded secrets using tools like TruffleHog.
Remediation
CLI
openstack server unset --property <sensitive_key> <instance_id>Terraform
# Terraform: use Barbican for secrets instead of metadata
# Store secret in Barbican
resource "openstack_keymanager_secret_v1" "db_password" {
name = "database-password"
payload = random_password.db_password.result
payload_content_type = "text/plain"
secret_type = "passphrase"
}
resource "random_password" "db_password" {
length = 32
special = true
}
# Instance WITHOUT sensitive data in metadata
resource "openstack_compute_instance_v2" "secure_instance" {
name = "app-server"
image_id = var.image_id
flavor_id = var.flavor_id
key_pair = var.key_pair_name
security_groups = ["app-sg"]
# Safe metadata (non-sensitive labels only)
metadata = {
environment = "production"
application = "web-app"
cost_center = "engineering"
barbican_secret_id = openstack_keymanager_secret_v1.db_password.secret_ref
}
network {
name = "private-network"
}
# Use cloud-init to retrieve secrets from Barbican
user_data = <<-EOF
#!/bin/bash
# Retrieve database password from Barbican
SECRET_REF="${openstack_keymanager_secret_v1.db_password.secret_ref}"
DB_PASSWORD=$(openstack secret get "$SECRET_REF" --payload -f value)
# Configure application with retrieved secret
echo "DATABASE_URL=<scheme>://user:$DB_PASSWORD@db-host/dbname" > /etc/app/config.env
chmod 600 /etc/app/config.env
EOF
}
# ANTI-PATTERN: DO NOT DO THIS
# resource "openstack_compute_instance_v2" "insecure_instance" {
# metadata = {
# db_password = "hardcoded-secret-123" # NEVER store secrets in metadata
# api_key = "sk-1234567890abcdef" # Exposed via metadata service
# }
# }Other
Check Code
import json
from typing import List
from prowler.lib.check.models import Check, CheckReportOpenStack
from prowler.lib.utils.utils import (
SecretsScanError,
annotate_verified_secrets,
detect_secrets_scan_batch,
)
from prowler.providers.openstack.services.compute.compute_client import compute_client
class compute_instance_metadata_sensitive_data(Check):
"""Ensure compute instance metadata does not contain sensitive data like passwords or API keys."""
def execute(self) -> List[CheckReportOpenStack]:
findings: List[CheckReportOpenStack] = []
secrets_ignore_patterns = compute_client.audit_config.get(
"secrets_ignore_patterns", []
)
validate = compute_client.audit_config.get("secrets_validate", False)
instances = list(compute_client.instances)
# Collect one payload per instance (its metadata) and scan them all in
# batched Kingfisher invocations instead of one subprocess per instance.
def payloads():
for index, instance in enumerate(instances):
if instance.metadata:
yield index, json.dumps(dict(instance.metadata), indent=2)
scan_error = None
try:
batch_results = detect_secrets_scan_batch(
payloads(), excluded_secrets=secrets_ignore_patterns, validate=validate
)
except SecretsScanError as error:
batch_results = {}
scan_error = error
for index, instance in enumerate(instances):
report = CheckReportOpenStack(metadata=self.metadata(), resource=instance)
report.status = "PASS"
report.status_extended = f"Instance {instance.name} ({instance.id}) metadata does not contain sensitive data."
if instance.metadata:
if scan_error:
report.status = "MANUAL"
report.status_extended = (
f"Could not scan instance {instance.name} ({instance.id}) "
f"metadata for secrets: {scan_error}; manual review is "
"required."
)
findings.append(report)
continue
original_metadata_keys = list(instance.metadata.keys())
detect_secrets_output = batch_results.get(index)
if detect_secrets_output:
# Map line numbers back to metadata keys using the parallel list
# Line numbering: line 1 = "{", line 2 = first key-value, etc.
secrets_string = ", ".join(
[
f"{secret['type']} in metadata key '{original_metadata_keys[secret['line_number'] - 2]}'"
for secret in detect_secrets_output
if 0
<= secret["line_number"] - 2
< len(original_metadata_keys)
]
)
report.status = "FAIL"
report.status_extended = f"Instance {instance.name} ({instance.id}) metadata contains potential secrets -> {secrets_string}."
annotate_verified_secrets(report, detect_secrets_output)
else:
report.status_extended = f"Instance {instance.name} ({instance.id}) has no metadata (no sensitive data exposure risk)."
findings.append(report)
return findings