Security by Obscurity and Accidental Protection

Security by Obscurity and Accidental Protection

Reading time1 min
#devops#security#kubernetes#terraform

Security by Obscurity and Accidental Protection

Security by obscurity means a system is safe only as long as nobody knows a detail about it: a hostname, a port, a URL path, a bucket name. Kerckhoffs's principle says the opposite should hold: a system should stay secure even if everything about it except the key is public.

Obscurity is not useless. Moving SSH off port 22 cuts log noise from untargeted bots. An unguessable bucket name makes enumeration harder. The problem is when obscurity is the only thing in the way, and nobody knows that.

Accidental protection

The more dangerous version is protection that nobody designed. Something blocks access, but by accident:

  • An admin route returns 404 because the ingress path prefix is wrong.
  • An internal API is unreachable only because a security group rule was never added.
  • A staging host has no public DNS record, so "nobody can find it".
  • An old load balancer has an IP allowlist that nobody remembers, and the new one will not.
  • A service listens on a port that the cloud firewall happens to block.

None of these are written down, tested or owned. They disappear during normal work: someone fixes the path, migrates to a new load balancer, replaces the ingress controller, or opens a port for an unrelated reason. Nothing alerts, because nothing was meant to be there.

Controller migrations are a common trigger right now. The community ingress-nginx project was retired in March 2026, and many teams are moving to Gateway API or other controllers. Path matching, rewrites, default backends and annotation-based auth do not carry over one to one. Anything that was protected by a routing quirk changes behaviour during that move.

How hidden things get found

Assume an attacker knows more about your external surface than your documentation does.

  • Certificate Transparency. Publicly trusted TLS certificates are logged in public CT logs, and browsers require it. Every hostname on such a certificate, including staging-admin.example.com, is searchable.
  • Internet-wide scanners. Shodan, Censys and similar services index open ports, banners and TLS certificates across the IPv4 space continuously.
  • Client code. JavaScript bundles and mobile apps contain API base URLs, feature flags and sometimes internal hostnames.
  • Public repos and docs. Old README files, Terraform examples and CI configs leak hostnames and paths.
  • Wordlists. Paths like /admin, /actuator, /debug and /.git/ are tried by every scanner.

Find what you depend on

Start with an inventory from the outside in. List public entry points from the source of truth, not from memory:

# internet-facing AWS load balancers
aws elbv2 describe-load-balancers \
  --query 'LoadBalancers[?Scheme==`internet-facing`].[LoadBalancerName,DNSName]' \
  --output table

# Kubernetes services and routes exposed to the outside
kubectl get svc -A --field-selector spec.type=LoadBalancer
kubectl get ingress -A
kubectl get gateways,httproutes -A   # if Gateway API CRDs are installed

Then check which of your hostnames are public knowledge already:

curl -s 'https://crt.sh/?q=%25.example.com&output=json' \
  | jq -r '.[].name_value' | sort -u

For each entry point, ask one question: what stops an unauthenticated request from the internet? Acceptable answers are authentication, a network control with an owner, or "it is meant to be public". "Nobody knows the URL" and "it returns 404 right now" are findings.

Make controls explicit

Authenticate at the application or at an identity-aware proxy. oauth2-proxy, a cloud IAP or the auth features of your gateway. Not a hidden path.

Default-deny network policy in Kubernetes, then allow what is needed:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: my-app
spec:
  podSelector: {}
  policyTypes: ["Ingress"]

Network policies only work if the CNI plugin enforces them. Check that before you rely on it.

Block public access to storage at the account level. New S3 buckets have Block Public Access on and ACLs disabled by default since April 2023, but older buckets and accounts may not. Set it once for the whole account:

resource "aws_s3_account_public_access_block" "this" {
  block_public_acls       = true
  ignore_public_acls      = true
  block_public_policy     = true
  restrict_public_buckets = true
}

Two mistakes in common examples: the acl argument on aws_s3_bucket is deprecated since AWS provider v4, and a bucket policy with Principal = "*" plus an aws:SourceAccount condition does not mean "only my account". That condition key is set for requests made by AWS services on your behalf, so the policy reads as public while doing something else. Grant access to specific IAM principals instead.

Test the negative path

Controls that are not tested become accidental too. Add checks that assert what must fail:

#!/usr/bin/env bash
set -euo pipefail

check() {
  local url=$1 expected=$2
  local code
  code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "$url" || true)
  if [[ ! " $expected " == *" $code "* ]]; then
    echo "FAIL $url returned $code, expected one of: $expected"
    exit 1
  fi
}

check https://api.example.com/admin "401 403"
check https://api.example.com/actuator/env "401 403 404"
check https://my-app-bucket.s3.amazonaws.com/ "403"

Run it from outside your network on a schedule, and from CI after changes to ingress, gateway or firewall config.

Checklist

  • Inventory of public entry points generated from cloud and cluster APIs.
  • For each one, a named control and an owner.
  • CT log search for your domains, reviewed against the inventory.
  • Authentication on every non-public endpoint, independent of routing.
  • Default-deny network policies with an enforcing CNI.
  • Account-level S3 Block Public Access.
  • Negative tests that fail when something becomes reachable.
  • Extra review during load balancer, ingress or gateway migrations.