8 minute read

Background

Virtualization workloads commonly have to access services on the physical network. Those same users also already have templated images available, with configured IP addresses. Thus, relying on the cluster default network is not an option - only secondary networks are.

Let’s take this a step further; it is good practice to use micro-segmentation to mitigate supply chain attacks - i.e. define using policy exactly what services can a workload access / be accessed from. Thus, even if a component is compromised, it wouldn’t be able to contact the outside world.

The combination of these two features is quite common, and useful. And up to now, impossible to have in Openshift: there simply wasn’t support for Kubernetes NetworkPolicy on secondary networks, least of all on the SDN solution offered by Openshift.

This blog post will explain how to configure a secondary L2 overlay connected to a physical network, and how to specify exactly what the workloads attached to these physical networks can do - i.e. which services can they access / which services get to access them.

Requirements

  • an Openshift 4.14 cluster
  • kubernetes-nmstate >= 2.2.15
  • CNO configuration:
    • enabled multi-network
    • enabled multi-net policies
    • OVN-Kubernetes plugin
  • a postgreSQL DB installed, available in the physical network

Scenario

In this scenario we will deploy workloads (both pods and VMs) requiring access to a relational DB reachable via the physical network (i.e. deployed outside Kubernetes).

For that we will deploy a secondary network.

Overlay network perspective

The VMs will be connected to two different networks: the cluster’s default network, owned and managed by Kubernetes, granting the VMs access to the internet, and an additional secondary network, named tenantblue, implemented by OVN-Kubernetes, and connected to the physical network, through which it will access the database deployed outside Kubernetes.

Both VMs (the the DB) will be available on the same subnet (192.168.200.0/24). The diagram below depicts the scenario explained above.

overlay-network-view

Physical network perspective

underlay-network-view

Setting up the database

We assume the DB is available on the localnet network.

Let’s just create a new user and database, and grant access to that user to manipulate the database:

sudo -u postgres psql
➜  ~ sudo -u postgres psql
psql (15.4)
Type "help" for help.

postgres=# CREATE USER splinter WITH PASSWORD 'cheese';
CREATE ROLE
postgres=# CREATE DATABASE turtles OWNER splinter;
CREATE DATABASE
postgres=# grant all privileges on database turtles to splinter;
GRANT
postgres=# \connect turtles
You are now connected to database "turtles" as user "postgres".
turtles=# GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO splinter;
GRANT
turtles=# \q

Allowing database remote access

We also need to ensure that the database is configured to allow incoming connections on the secondary network. For that, we need to update the host based configuration of the database. Locate your postgres pg_hba.conf file, and ensure the following entry is there:

host    turtles         splinter        192.168.200.0/24	md5

That will allow clients (on the 192.168.200.0/24 subnet) to login to all databases using username and password.

NOTE: Remember to reload / restart your DB service if you’ve reconfigured it

Provision the database

Now that the database and user are created, let’s provision a table and some data. Paste the following into a file - let’s call it data.sql:

CREATE TABLE ninja_turtles (
    user_id serial PRIMARY KEY,
    name VARCHAR ( 50 ) UNIQUE NOT NULL,
    email VARCHAR ( 255 ) UNIQUE NOT NULL,
    weapon VARCHAR ( 50 ) NOT NULL,
    created_on TIMESTAMP NOT NULL DEFAULT now()
);

INSERT INTO ninja_turtles(name, email, weapon) values('leonardo', 'leo@tmnt.org', 'swords');
INSERT INTO ninja_turtles(name, email, weapon) values('donatello', 'don@tmnt.org', 'a stick');
INSERT INTO ninja_turtles(name, email, weapon) values('michaelangello', 'mike@tmnt.org', 'nunchuks');
INSERT INTO ninja_turtles(name, email, weapon) values('raphael', 'raph@tmnt.org', 'twin sai');

Finally provision the data file:

sudo -u postgres psql -f data.sql turtles

Creating an overlay network connected to a physical network

The first thing we need to do is configure the underlay for our “new” network; we will use an NMState node network configuration policy for that.

apiVersion: nmstate.io/v1
kind: NodeNetworkConfigurationPolicy
metadata:
  name: ovs-share-same-gw-bridge
spec:
  nodeSelector:
    node-role.kubernetes.io/worker: ''
  desiredState:
    ovn:
      bridge-mappings:
      - localnet: tenantblue
        bridge: br-ex

The policy above will be applied only on worker nodes - notice the node selector used - and will configure a mapping in the OVS system to connect the traffic from the tenantblue network to the br-ex OVS bridge, which is already configured - and managed - by OVN-Kubernetes. Do not touch any of its settings - all you can do is point more networks to it.

Now we need to provision the logical networks in Openshift. For that, please use the following manifests:

---
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
  name: tenantblue
spec:
    config: '{
        "cniVersion": "0.3.1",
        "name": "tenantblue",
        "netAttachDefName": "default/tenantblue",
        "topology": "localnet",
        "type": "ovn-k8s-cni-overlay",
        "logFile": "/var/log/ovn-kubernetes/ovn-k8s-cni-overlay.log",
        "logLevel": "5",
        "logfile-maxsize": 100,
        "logfile-maxbackups": 5,
        "logfile-maxage": 5
    }'

NOTE: the spec.config.name of the NAD - i.e. tenantblue - must match the localnet attribute of the ovn bridge mapping provisioned above.

Now we just need to provision the VM workload:

---
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  name: vm-workload
spec:
  running: true
  template:
    metadata:
      labels:
        role: web-client
    spec:
      domain:
        devices:
          disks:
            - name: containerdisk
              disk:
                bus: virtio
            - name: cloudinitdisk
              disk:
                bus: virtio
          interfaces:
          - name: default
            masquerade: {}
          - name: tenantblue-network
            bridge: {}
        machine:
          type: ""
        resources:
          requests:
            memory: 1024M
      networks:
      - name: default
        pod: {}
      - name: tenantblue-network
        multus:
          networkName: tenantblue
      terminationGracePeriodSeconds: 0
      volumes:
        - name: containerdisk
          containerDisk:
            image: quay.io/containerdisks/fedora:38
        - name: cloudinitdisk
          cloudInitNoCloud:
            networkData: |
              version: 2
              ethernets:
                eth0:
                  dhcp4: true
                eth1:
                  gateway4: 192.168.200.1
                  addresses: [ 192.168.200.20/24 ]
            userData: |-
              #cloud-config
              password: fedora
              chpasswd: { expire: False }
              packages:
                - postgresql

Access the DB from the virtual machine

PGPASSWORD=cheese psql -U splinter -h 192.168.200.1 turtles -c 'select * from ninja_turtles'
 user_id |      name      |     email     |  weapon  |         created_on
---------+----------------+---------------+----------+----------------------------
       1 | leonardo       | leo@tmnt.org  | swords   | 2023-12-04 13:53:37.004108
       2 | donatello      | don@tmnt.org  | a stick  | 2023-12-04 13:53:37.004108
       3 | michaelangello | mike@tmnt.org | nunchuks | 2023-12-04 13:53:37.004108
       4 | raphael        | raph@tmnt.org | twin sai | 2023-12-04 13:53:37.004108
(4 rows)

Restrict traffic on the secondary network

So far, we have access from the VM to the DB running outside Kubernetes. To illustrate the network policy scenario, we will first modernize our monolithic VM by extracting part of its functionality (access to the database) to a standalone Pod. This pod will expose the DB’s contents via a REST API.

Thus, in essence, we will have:

  • 1 VM: accesses the DB’s contents indirectly, by querying the pod over its REST API
  • 1 pod: DB client; exposes the DB contents over a RESTful CRUD API
  • 1 DB hosted on the physical network
  • 2 multi-network policies
    • one granting access to the TCP port 5432 (port over which the PostgreSQL DB is listening) in the physical network and allowing connections from its subnet to port 9000; this policy applies only to the pod
    • one granting access to the pod’s TCP port 9000 (port where the RESTful API is listening); this policy will apply only to the VM

All other traffic will be rejected.

The following diagram depicts the scenario: multi-net-policy scenario

To implement this use case, we will re-use the network attachment definition, NMState’s NodeNetworkConfigurationPolicy, and VM available in this section.

We will need to provision the database adapter workload; for that, provision the following yaml:

apiVersion: v1
kind: Pod
metadata:
  name: turtle-db-adapter
  annotations:
    k8s.v1.cni.cncf.io/networks: '[
      {
        "name": "tenantblue",
        "ips": [ "192.168.200.10/24" ]
      }
    ]'
  labels:
    role: db-adapter
spec:
  containers:
  - name: db-adapter
    env:
    - name: DB_USER
      value: splinter
    - name: DB_PASSWORD
      value: cheese
    - name: DB_NAME
      value: turtles
    - name: DB_IP
      value: "192.168.200.1"
    - name: HOST
      value: "192.168.200.10"
    - name: PORT
      value: "9000"
    image: ghcr.io/maiqueb/rust-turtle-viewer:main
    ports:
    - name: webserver
      protocol: TCP
      containerPort: 9000
    securityContext:
      runAsUser: 1000
      privileged: false
      seccompProfile:
        type: RuntimeDefault
      capabilities:
        drop: ["ALL"]
      runAsNonRoot: true
      allowPrivilegeEscalation: false

Note: the image specified above was generated by automation on the following repo. It is nothing but a toy.

We will need to provision the following MultiNetworkPolicies:

---
apiVersion: k8s.cni.cncf.io/v1beta1
kind: MultiNetworkPolicy
metadata:
  name: db-adapter
  annotations:
    k8s.v1.cni.cncf.io/policy-for: default/tenantblue
spec:
  podSelector:
    matchLabels:
      role: db-adapter
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - ipBlock:
        cidr: 192.168.200.0/24
    ports:
    - protocol: TCP
      port: 9000
  egress:
  - to:
    - ipBlock:
      cidr: 192.168.200.0/24
    ports:
    - protocol: TCP
      port: 5432
---
apiVersion: k8s.cni.cncf.io/v1beta1
kind: MultiNetworkPolicy
metadata:
  name: web-client
  annotations:
    k8s.v1.cni.cncf.io/policy-for: default/tenantblue
spec:
  podSelector:
    matchLabels:
      role: web-client
  policyTypes:
  - Ingress
  - Egress
  egress:
  - to:
    - ipBlock:
      cidr: 192.168.200.0/24
    ports:
    - protocol: TCP
      port: 9000
  ingress: []

After provisioning the multi-network policies above, you will see the VM workload is no longer able to access the DB (the command below hangs):

[fedora@vm-workload ~]$ PGPASSWORD=cheese psql -Usplinter -h 192.168.200.1 turtles

But, it can access the db-adapter workload we’ve just deployed:

curl -H 'Content-Type: application/json' 192.168.200.10:9000/turtles | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   481  100   481    0     0   6562      0 --:--:-- --:--:-- --:--:--  6589
{
  "Ok": [
    {
      "user_id": 1,
      "name": "leonardo",
      "email": "leo@tmnt.org",
      "weapon": "swords",
      "created_on": "2023-12-12T12:46:26.455183"
    },
    {
      "user_id": 2,
      "name": "donatello",
      "email": "don@tmnt.org",
      "weapon": "a stick",
      "created_on": "2023-12-12T12:46:26.457766"
    },
    {
      "user_id": 3,
      "name": "michaelangello",
      "email": "mike@tmnt.org",
      "weapon": "nunchuks",
      "created_on": "2023-12-12T12:46:26.458432"
    },
    {
      "user_id": 4,
      "name": "raphael",
      "email": "raph@tmnt.org",
      "weapon": "twin sai",
      "created_on": "2023-12-12T12:46:26.459101"
    }
  ]
}

Conclusions

In this blog post we have seen how the user can deploy a VM workload attached to a secondary network that requires access to a service deployed outside Kubernetes - in this scenario, an SQL database.

We have also seen how to use network policies to restrict access in this secondary network, only granting access to the DB to selected workloads, and also ensuring our VM workload can only access applications on a particular port on the network.

Updated: