Skip to content

Firewalls

You want a service to accept only the traffic you named. On AWS that is a Security Group. Here it is a NetworkPolicy, and the console wraps it in a form that writes one rule in front of one service.

The mental model transfers directly. What differs is when the policy starts to apply, and what a project starts with.

Prerequisites

  • A project with at least one service that selects pods
  • The port the pods listen on - not always the port the service publishes

The step-by-step guide and the console walkthrough cover the operations. The rest of this page explains how it fits together.

A new project starts closed, except for web traffic

A project you create, the project of a private subnet included, receives eleven firewalls in its first seconds. Together they drop every inbound connection except the ones below. Outbound traffic is not restricted.

Firewall Admits
default-deny-ingress nothing - it selects every workload and lists no rule
allow-same-namespace workloads in the same project, on any port
allow-same-tenant workloads in every project of your tenant, on any port
allow-from-openshift-ingress the platform's routers, so a Route works
allow-from-openshift-monitoring the platform's metrics collection
allow-from-openshift-console the console, on TCP 4444 only, for its web terminal
allow-from-operators the operators that manage the VMs and caches you create
allow-from-cnpg the PostgreSQL operator
allow-from-mariadb the MariaDB operator
allow-from-velero the platform's backup and restore service
allow-http-https TCP 80, 443, 8080 and 8443 from anywhere

The four platform-service firewalls select the platform's namespaces by a label only the platform can set, so they admit no other tenant. A project created before they existed holds one firewall named allow-from-platform-operators in their place, which does the same.

oc -n <project> get networkpolicy

They are created once and are yours from then on: edit or delete any of them and the platform does not put it back. A workload on another port - a database, SSH - is reachable from the projects of your tenant and from nowhere else until you add a firewall for it.

Two kinds of project have none of these:

  • The project of a public subnet. It is created open on purpose: a workload there is reachable from the internet on every port it listens on. See VM with a direct public IP.
  • A project created before these defaults existed. Its list is empty, or holds only what you added yourself.

The next section describes what happens in such a project.

The first firewall flips the default to deny

flowchart TB
    subgraph AFTER["after one firewall selects it"]
        direction TB
        S2["your service"]
        A2["what your rule names"] -->|allowed| S2
        B2["everything else"] -.->|dropped| S2
    end
    subgraph BEFORE["before the first firewall"]
        direction TB
        S1["your service"]
        A1["same project"] -->|allowed| S1
        B1["anywhere on the cluster"] -->|allowed| S1
        C1["the internet"] -->|allowed| S1
    end

Until a service is selected by a policy, it accepts anything that can route to it. The moment one selects it, it accepts only what the rules allow.

There is no partial state and no warning. One rule flips the default from allow to deny for the workloads it selects.

Health checks and metrics need allow rules of their own

Only the port you name gets through. A service that also serves health checks or metrics on another port needs a second firewall for that port, or those checks start failing the moment the first rule exists.

A firewall selects pods, not services

A NetworkPolicy has no concept of a service. podSelector matches pods directly.

The form asks you to pick a service and then copies that service's own selector into the policy, so the two agree. That is the part hand-written policies most often get wrong: a selector matching nothing produces a policy that protects nothing and looks identical to a working one in any list.

How far an allow rule reaches

A rule admits traffic from one of four boundaries. Each boundary contains the one before it, so a wider rule also admits everything a narrower one does.

flowchart TB
    subgraph NET["the internet"]
        subgraph CLUSTER["the cluster - every customer's projects"]
            subgraph TENANT["your tenant - all your projects"]
                subgraph PROJECT["your project"]
                    S["your service"]
                end
            end
        end
    end
Boundary Who is inside it from Use for
Your project workloads in the same project podSelector: {} a database or cache only its own application uses
Your tenant workloads in every project of your tenant namespaceSelector on hoolia.eu/tenant: <tenant> a service your other projects call - dev, staging, a shared database
The cluster every project on the cluster, other customers' projects included namespaceSelector: {} a service you offer to other customers on the platform
The internet any source address ipBlock: {cidr: 0.0.0.0/0} a public service

Start with the narrowest that lets the application work. Narrow is cheap to widen later; wide is what gets exploited in the meantime.

Your project and your tenant

A project is one namespace. A tenant is the set of projects that belong to one customer or one team: the home project, which carries the tenant's name, and every project created under it. The platform labels each of them hoolia.eu/tenant: <tenant>. A project with an empty TENANT column belongs to no tenant.

oc get projects -L hoolia.eu/tenant

Only the platform sets that label. A project that asks for it at creation is refused, and the label of an existing project cannot be changed, so a rule that selects hoolia.eu/tenant: <tenant> admits your projects and no one else's. That makes the tenant the widest boundary that still contains only workloads you control.

The cluster and the internet

A public cloud cluster is shared between customers. namespaceSelector: {} selects every project on it, so a rule with that source admits the workloads of customers you do not know. It is not a way to say "inside my own environment" - the tenant label is.

ipBlock: {cidr: 0.0.0.0/0} matches every source address, the addresses of other projects on the cluster included. It admits everything the cluster boundary admits, and the internet as well.

A firewall admits traffic; it does not publish a service. A source outside the cluster reaches a workload only when a Route, a load balancer or a public subnet gives it a path there.

What a new project admits at each boundary

Boundary Admitted by default By which firewall
Your project every port allow-same-namespace
Your tenant every port allow-same-tenant
The cluster TCP 80, 443, 8080 and 8443 allow-http-https
The internet TCP 80, 443, 8080 and 8443 allow-http-https

Deleting one of those firewalls closes its boundary, and adding a rule opens one for a single service and port. Open or close a boundary has the commands for each.

Changes apply immediately

There is no staged rollout and no dry run against live traffic. A mistake drops real requests from the moment it is applied, and deleting the rule restores the previous behaviour equally quickly.

The list cannot tell you whether a rule is correct - one that is too narrow looks exactly like one that is right. Test from where the service is meant to be reachable.