You’ll receive an email confirming your submission.
Our team will contact you within 24–72 hours, depending on the complexity of your request.
By submitting, you agree to our [Privacy Policy] and consent to receive updates or consultation support from Open Reach Tech.
Please select the privacy consent checkbox.

components..title

components..description

components..title

components..description

You’ll receive an email confirming your submission.
Our team will contact you within 24–72 hours, depending on the complexity of your request.
By submitting, you agree to our [Privacy Policy] and consent to receive updates or consultation support from Open Reach Tech.
Please select the privacy consent checkbox.

I Thought UFW Was Protecting My Server. I Was Wrong

A journey into how one small assumption in networking and firewall configuration can unintentionally expose internal services to the Internet — and the real-world lessons I learned about Docker, UFW, and* ***false sense of security*** *in production systems.

Banner of I Thought UFW Was Protecting My Server. I Was Wrong

Everything Looked Fine

I had already enabled UFW.

ufw status showed that the port was denied.

There were no unusual warnings in the logs.

Deployments were running normally.

Monitoring dashboards were all green.

Everything looked safe.

Until one day, I tested the service from an external network… and discovered that an internal service running in production was still publicly accessible from the Internet.

The scariest part was:

  • There were no obvious signs that this was happening.
  • No alerts.
  • No errors.
  • No failed deployments.

It was simply:

  • I believed the system was protected
  • while in reality, it wasn’t

That moment made me realize something important:

In infrastructure engineering, the most dangerous thing is not always a bug.

Sometimes, it’s a false sense of security.


Internal Services Were Never Meant to Be Public

Modern systems often rely on many internal services such as:

  • PostgreSQL
  • Redis
  • Elasticsearch
  • Kafka
  • RabbitMQ
  • Internal Admin APIs
  • Monitoring services

These services are usually intended only for:

  • backend applications
  • batch jobs
  • internal networks
  • or developers connecting through VPNs or SSH tunnels

They were never designed to be publicly accessible from the Internet.

But many production systems accidentally expose them without realizing it.

For example:

ports:
  - "9200:9200"

At first glance, this configuration looks harmless.

But in many cases, it effectively means the service is listening on:

0.0.0.0

Which means:

  • all network interfaces
  • including the server’s public interface

If the machine has a public IP, there’s a good chance the service is reachable from the Internet.

And the most dangerous part is:

Many developers don’t realize this at all.


“But I Already Blocked the Port With UFW”

That was exactly what I thought too.

I believed that simply running:

ufw deny 9200

was enough to protect Elasticsearch.

Everything looked correct:

  • UFW was enabled
  • the rule existed
  • ufw status showed DENY

But when I tested the service from an external network:

curl http://public-ip:9200

the service still responded normally.

That was when I learned something important:

Docker doesn’t just “open a port.”

When publishing container ports, Docker also manipulates iptables rules to route traffic to containers.

As a result, Docker-published ports can sometimes bypass the behavior developers expect from UFW.

And that led me to one of the biggest lessons from this experience:

Firewall rules are not proof of security.

The only thing that truly matters is:

Whether external traffic can actually reach the service or not.


The Internet Is Never Quiet

One thing many developers underestimate is this:

The Internet is constantly scanning your infrastructure.

Not because you are famous.

Not because someone is specifically targeting you.

But because Internet-wide scanning has become almost completely automated.

There are countless:

  • scanners
  • crawlers
  • opportunistic bots
  • mass-scanning tools

constantly probing:

  • Redis
  • Elasticsearch
  • PostgreSQL
  • MongoDB
  • admin panels
  • internal APIs

If a service is publicly exposed without proper protection, being discovered is usually just a matter of time.


The Dangerous Part Isn’t Just That the Service Is Public

The scariest part is often not the exposure itself.

It’s the fact that:

  • nobody realizes the service is public
  • because everyone assumes the firewall is protecting it

That’s why many incidents are only discovered after:

  • data disappears
  • servers get abused
  • or infrastructure costs suddenly spike

There have already been countless real-world incidents where:

  • Elasticsearch indices were wiped
  • Redis databases were flushed
  • databases received ransom notes
  • internal services were abused for crypto mining

And most of these incidents didn’t start with:

  • sophisticated malware
  • zero-day exploits
  • advanced attackers

They started with:

  • default configurations
  • convenience during development
  • incorrect assumptions
  • lack of real-world verification

“Configuration Looks Safe” Doesn’t Mean “Actually Safe”

This became the biggest lesson I learned from the experience.

In production engineering:

“Configuration looks safe”

does not mean:

“The system is actually safe”

You can have:

  • firewalls
  • deny rules
  • monitoring
  • green dashboards
  • beautiful infrastructure diagrams

But if external traffic can still reach the service, then all of those assumptions become meaningless.

Security should never be verified through:

  • dashboards
  • configuration files
  • assumptions

It should be verified through:

  • external testing
  • real network validation
  • defense in depth

What I Started Doing Differently

After this experience, I changed the way I approach infrastructure setup.

1. Bind Internal Services to 127.0.0.1

Instead of:

ports:
  - "9200:9200"

I started using:

ports:
  - "127.0.0.1:9200:9200"

This ensures the service is only accessible locally.


2. Avoid Publishing Internal Ports Unless Absolutely Necessary

Many internal services simply do not need public Internet access.

If only containers need to communicate internally, then:

  • Docker internal networks
  • private subnets
  • service discovery

are usually more than enough.


3. Don’t Rely on Host Firewalls Alone

UFW is useful.

But it should never be the only layer of protection.

I started relying more on:

  • cloud security groups
  • private networking
  • VPNs
  • zero-trust access
  • network segmentation

Defense in depth is always stronger than single-layer protection.


4. Always Verify From an External Network

This became the most important habit.

Not:

curl localhost:9200

But testing from:

  • mobile networks
  • external VPS instances
  • real external Internet connections

If external traffic cannot reach the service, only then is it truly trustworthy.


The Biggest Lesson

The biggest lesson I learned was not:

“Docker networking is complicated.”

It was:

Infrastructure assumptions are dangerous.

Many production incidents do not start with catastrophic mistakes.

They start with small assumptions that nobody bothered to verify.

And sometimes, the most dangerous thing is not that:

  • the service is public

But that:

  • you believe it isn’t.

Conclusion

After this experience, I started looking at infrastructure differently.

No longer asking:

“Did I configure this correctly?”

But instead asking:

“What happens if the Internet actually tries to reach this system?”

That is the difference between:

  • configuration
  • and actual security

And sometimes, a tiny assumption like:

ports:
  - "9200:9200"

is enough to accidentally turn an internal service into a public one without anyone realizing it.