# Lab walkthrough: Securing S3 Object Storage with BIG-IP Next for Kubernetes

**URL:** <https://community.f5.com/t/lab-walkthrough-securing-s3-object-storage-with-big-ip-next-for-kubernetes/77461>\
**Category:** F5 Technical Articles\
**Tags:** application-delivery, cloud-native\
**Created:** [October 5, 2026, 3:00pm UTC](https://community.f5.com/t/lab-walkthrough-securing-s3-object-storage-with-big-ip-next-for-kubernetes/77461 "2026-10-05T15:00:57Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![momahdy](https://d1p9zq3aats0t8.cloudfront.net/user_avatar/community.f5.com/momahdy/32/16251_2.png) [@momahdy](https://community.f5.com/u/momahdy)\
**Post date:** [October 5, 2026, 3:00pm UTC](https://community.f5.com/t/lab-walkthrough-securing-s3-object-storage-with-big-ip-next-for-kubernetes/77461/1 "2026-10-05T15:00:57Z")

</div>

## Introduction

MinIO gives you an S3-compatible object store that runs anywhere Kubernetes runs. That makes it a common choice for AI training data, model checkpoints, data lakes, backups, container registries, and log archives in on-prem, edge, and air-gapped environments.

The tradeoff is that you now own everything a cloud provider used to handle for you at the edge: how the endpoint is exposed, how traffic is load balanced, how attacks are absorbed, and how tenants stay separated.

This lab walks through putting BIG-IP Next for Kubernetes (BNK) in front of a MinIO deployment and solving four specific challenges,

- **Networking** : S3 traffic is long-lived and high-volume. Large multipart uploads, thousands of parallel GETs from training jobs, and bucket addressing (virtual-host vs. path style) break ingress controllers built for small REST calls. Replication and tiering also need a stable outbound source address.
- **DDoS protection** : MinIO authenticates requests but does not absorb attacks. SYN floods, slow connections, and TLS abuse consume node resources before any policy runs. Valid-but-expensive requests (recursive `ListObjectsV2`, abandoned multipart uploads) and credential stuffing against long-lived access keys are just as damaging.
- **Firewalling** : MinIO IAM decides _who can do what to which bucket_, not _who can reach the endpoint at all_. There is no source subnet, geo/ASN, port, or rate control. Kubernetes NetworkPolicy is CNI-dependent and offers little for north-south traffic.
- **Namespace enforcement** : Kubernetes namespaces alone do not provide network isolation, and without additional controls, shared egress can make per-tenant attribution, policy enforcement, quotas, and upstream allowlisting difficult.

Putting BIG-IP Next for Kubernetes (BNK) in the traffic path provides a common, declarative way to address these networking, security, and tenant-isolation requirements.

This article walks the build: two MinIO tenants, one BNK Gateway, security policy attached as Kubernetes objects, and a telemetry path into Wazuh (SIEM solution) that produces severity-scored alerts on real attacker traffic.

## Lab walkthrough

In this section we go over the lab overview and the steps to get this lab working

 ![Figure 1: Overall Architecture](https://d20hrnpixdzcsd.cloudfront.net/original/3X/c/a/caf97053ae02b299b16261befedf61664c8cb634.png)

This walkthrough starts **after** the BNK platform itself is running.

**What are the installation requirements**

| Requirement | Why it matters |
| --- | --- |
| Licensed CNE image pull secret | BNK images are in a private F5 registry. No secret, no controller pods. |
| JWT license token | TMM will not program a dataplane without it. |
| Multus CNI | TMM attaches **secondary** interfaces for its dataplane. Without Multus the Gateway VIP never comes up. |
| Dedicated external + internal dataplane subnets | TMM bridges traffic on its own L2 segments, separate from the pod network. |
| cert-manager | Injects the CA for the CRD conversion and admission webhooks. |
| Gateway API CRDs | BNK implements the standard Gateway API, plus its own `k8s.f5net.com` extensions. |

Let’s have a look at the created pods and gateway APIs,

```bash
~/cyber-range-lab$ kubectl get pods -n f5-cne-system
NAME READY STATUS RESTARTS AGE
f5-afm-85cf49d5d-smqbd 2/2 Running 0 47h
f5-analyzer-64cc6cf7fb-jp67w 2/2 Running 0 47h
f5-cne-controller-78b6644f8b-ttdns 3/3 Running 0 46h
f5-dssm-db-0 3/3 Running 0 47h
f5-dssm-db-1 3/3 Running 0 47h
f5-dssm-db-2 3/3 Running 0 47h
f5-dssm-sentinel-0 3/3 Running 0 47h
f5-dssm-sentinel-1 3/3 Running 0 47h
f5-dssm-sentinel-2 3/3 Running 0 47h
f5-tmm-flvx7 7/7 Running 0 47h
wazuhgov-collector-2tpg7 1/1 Running 0 47h
wazuhgov-collector-lhzm7 1/1 Running 0 47h
wazuhgov-collector-z44n2 1/1 Running 0 47h
~/cyber-range-lab$ kubectl get pods -n f5-cne-core
NAME READY STATUS RESTARTS AGE
crd-installer-pdpjr 0/2 Completed 0 47h
f5-coremond-54ww7 2/2 Running 0 47h
f5-coremond-9ghzh 2/2 Running 0 47h
f5-coremond-bvsks 2/2 Running 0 47h
f5-crdconversion-5886554c5b-kslcf 2/2 Running 0 46h
f5-ipam-ctlr-85cf6c9c44-97h9x 2/2 Running 0 47h
f5-lifecycle-operator-8f878986f-7z5hg 1/1 Running 0 47h
f5-observer-0 2/2 Running 0 47h
f5-observer-operator-7587b47664-m9jpf 2/2 Running 0 47h
f5-observer-receiver-0 2/2 Running 0 47h
f5-rabbit-75645cb787-c2qvh 2/2 Running 0 47h
f5-spk-csrc-fld82 2/2 Running 0 47h
f5-spk-csrc-lgv79 2/2 Running 0 47h
f5-spk-csrc-v5bkm 2/2 Running 0 47h
f5-spk-cwc-795fd7f866-4z54f 3/3 Running 0 47h
f5-toda-fluentd-865f5cf59-4grk7 1/1 Running 0 47h
otel-collector-79cfc58fb-xwnfs 1/1 Running 0 47h
~/cyber-range-lab$ kubectl get gatewayclass
NAME CONTROLLER ACCEPTED AGE
cyber-range-bnk-gatewayclass f5.com/f5-cne-system-f5-cne-controller True 47h

```

In the following subsections we will look at each component separately,

### **Step 1: The Gateway**

Standard Gateway API, with one BNK-specific addition: an explicit VIP via `addresses`. This is the address TMM will own on the external dataplane subnet.

```yaml

apiVersion: gateway.networking.k8s.io/v1

kind: Gateway

metadata:

  name: org-a-s3-gateway

  namespace: f5-bnk

spec:

  gatewayClassName: cyber-range-bnk-gatewayclass

  addresses:

    - type: IPAddress

      value: 10.0.10.241

  listeners:

    - name: https-deptx

      protocol: HTTPS

      port: 443

      hostname: "s3-deptx.org-a.range.local"

      allowedRoutes:

        namespaces:

          from: All

      tls:

        mode: Terminate

        certificateRefs:

          - name: org-a-tls-cert

    - name: https-depty

      protocol: HTTPS

      port: 443

      hostname: "s3-depty.org-a.range.local"

      allowedRoutes:

        namespaces:

          from: All

      tls:

        mode: Terminate

        certificateRefs:

          - name: org-a-tls-cert

```

Two listeners on the same port, separated by SNI hostname. TLS terminates at the gateway, which is what makes HTTP-aware policy and logging possible at all.

`allowedRoutes.namespaces.from: All` lets each tenant own its route in its own namespace, which matters for the next step.

### **Step 2: Routes to the tenants**

Each department owns its `HTTPRoute`, in its own namespace, pointing at its own MinIO Service:

```yaml

apiVersion: gateway.networking.k8s.io/v1

kind: HTTPRoute

metadata:

  name: dept-x-minio-route

  namespace: dept-x

spec:

  parentRefs:

    - name: org-a-s3-gateway

      namespace: f5-bnk

  hostnames:

    - "s3-deptx.org-a.range.local"

  rules:

    - backendRefs:

        - name: minio-x

          port: 9000

---

apiVersion: gateway.networking.k8s.io/v1

kind: HTTPRoute

metadata:

  name: dept-y-minio-route

  namespace: dept-y

spec:

  parentRefs:

    - name: org-a-s3-gateway

      namespace: f5-bnk

  hostnames:

    - "s3-depty.org-a.range.local"

  rules:

    - backendRefs:

        - name: minio-y

          port: 9000

```

The platform team owns the Gateway and the security policy. Application teams own their routes. Neither has to touch the others config, and the security controls apply regardless of what the tenant does with its route.

### **Step 3: Attach security policy**

This is where BNK extends beyond application traffic delivery to apply security controls declaratively through Kubernetes objects.

#### **Firewall**

```yaml

apiVersion: k8s.f5net.com/v1

kind: F5BigFwRulelist

metadata:

  name: s3-ingress-rules

  namespace: f5-bnk

spec:

  rule:

    - name: allow-internal

      action: accept

      ipProtocol: tcp

      source:

        addresses:

          - "10.0.0.0/8"

      destination:

        ports:

          - "443"

          - "9000"

      logging: true

    - name: deny-default

      action: drop

      ipProtocol: any

      logging: true

---

apiVersion: k8s.f5net.com/v1

kind: F5BigFwPolicy

metadata:

  name: s3-ingress-firewall

  namespace: f5-bnk

spec:

  rule:

    - name: use-ingress-rulelist

      ruleList: s3-ingress-rules

```

Note `logging: true` on both rules. That is what makes accepts and drops visible downstream.

#### **DDoS**

```yaml

apiVersion: k8s.f5net.com/v1

kind: F5BigDdosGlobal

metadata:

  name: s3-ddos-profile

  namespace: f5-bnk

spec:

  hslPublisher: wazuh-hsl-publisher

  vectors:

    synFlood:

      state: mitigation

      rateLimit: 10000

```

#### **The binding step, and the one constraint that will stop you**

Policies do nothing until a `SecPolicy` binds them to a target. The intuitive move is one SecPolicy listing all three. **The webhook rejects it:**

> When extensionRefs includes F5BigDdosGlobal, all targetRefs.kind must be GatewayClass. Gateway and EgressGateway are not allowed.

But `F5BigFwPolicy` and `F5BigLogProfile` require a **Gateway** target. The two sets are mutually exclusive, so you need two SecPolicy objects:

 ![image](https://d20hrnpixdzcsd.cloudfront.net/original/3X/3/d/3db3288e91e0bf098b55534e3896a77d579b4e02.png)

```yaml

apiVersion: gateway.k8s.f5.com/v1alpha1

kind: SecPolicy

metadata:

  name: org-a-s3-secpolicy-gateway

  namespace: f5-bnk

spec:

  targetRefs:

    - group: gateway.networking.k8s.io

      kind: Gateway

      name: org-a-s3-gateway

  extensionRefs:

    - group: k8s.f5net.com

      kind: F5BigFwPolicy

      name: s3-ingress-firewall

    - group: k8s.f5net.com

      kind: F5BigLogProfile

      name: wazuh-log-profile

---

apiVersion: gateway.k8s.f5.com/v1alpha1

kind: SecPolicy

metadata:

  name: org-a-s3-secpolicy-gatewayclass

  namespace: f5-bnk

spec:

  targetRefs:

    - group: gateway.networking.k8s.io

      kind: GatewayClass

      name: cyber-range-bnk-gatewayclass

  extensionRefs:

    - group: k8s.f5net.com

      kind: F5BigDdosGlobal

      name: s3-ddos-profile

```

### **Step 4: Telemetry into the SIEM**

The goal is that every HTTP transaction becomes a structured record in Wazuh.

 ![image](https://d20hrnpixdzcsd.cloudfront.net/original/3X/c/a/ca825f1fe5c2d62b1b0f380d412ddabe38430242.png)

#### **The publisher**

```yaml

apiVersion: k8s.f5net.com/v2

kind: F5BigLogHslpub

metadata:

  name: wazuh-hsl-publisher

  namespace: f5-bnk

spec:

  pool:

    - name: wazuh-manager-pool

      members:

        - address: "10.0.1.26"

          port: 514

  syslog:

    - name: wazuh-syslog

      pool: wazuh-manager-pool

      protocol: udp

      format: rfc5424

---

apiVersion: k8s.f5net.com/v2

kind: F5BigLogProfile

metadata:

  name: wazuh-log-profile

  namespace: f5-bnk

spec:

  publisher: wazuh-hsl-publisher

  firewall:

    enabled: true

    network:

      publisher: wazuh-hsl-publisher

      events:

        aclMatchAccept: true

        aclMatchDrop: true

        aclMatchReject: true

```

And that’s how the Wazuh namespace looks like

```bash
~/cyber-range-lab$ kubectl get pod -n wazuh
NAME READY STATUS RESTARTS AGE
wazuh-dashboard-75779586cc-lp4pn 1/1 Running 0 2d
wazuh-indexer-0 1/1 Running 0 2d
wazuh-manager-5ddd4c85c-474tk 1/1 Running 0 2d

```

#### **The iRule**

```yaml

apiVersion: k8s.f5net.com/v1

kind: F5BigCneIrule

metadata:

  name: wazuh-gateway-log-irule

  namespace: f5-bnk

spec:

  iRule: |

    when HTTP_REQUEST {

      set wazuhgov_client \[IP::client_addr\]

      set wazuhgov_method \[HTTP::method\]

      set wazuhgov_uri \[string range \[HTTP::uri\] 0 299\]

      set wazuhgov_host \[HTTP::host\]

    }

    when HTTP_RESPONSE {

      set wazuhgov_status \[HTTP::status\]

      set wazuhgov_action "allow"

      if { $wazuhgov_status >= 400 } { set wazuhgov_action "denied" }

      if { $wazuhgov_method eq "DELETE" } { set wazuhgov_action "delete_object" }

      set wazuhgov_hsl \[HSL::open -publisher f5-bnk-wazuh-hsl-publisher-hslpublisher\]

      HSL::send $wazuhgov_hsl "<134>WAZUHGOV {...}"

    }

```

Three things in that snippet are non-obvious and each one will silently produce zero logs:

- The `<134>` prefix is mandatory. `HSL::send` does not prepend a syslog priority header even when the publisher declares `format: rfc5424`, and `wazuh-remoted` discards any datagram without one. 134 is local0.info.

- The publisher name is the TMM-side object name, generated as `namespace-CRname-hslpublisher`, not the CR name. `HSL::open` returns an invalid handle silently if it does not resolve.

- `log local0.` does not work. The TMM container runs no syslogd. HSL is the only way out.

#### **Binding the iRule**

```yaml

apiVersion: gateway.k8s.f5.com/v1alpha1

kind: NetPolicy

metadata:

  name: org-a-s3-netpolicy-deptx

  namespace: f5-bnk

spec:

  targetRefs:

    - group: gateway.networking.k8s.io

      kind: Gateway

      name: org-a-s3-gateway

      sectionName: https-deptx

  extensionRefs:

    - group: k8s.f5net.com

      kind: F5BigCneIrule

      name: wazuh-gateway-log-irule

```

### Step 5: Turning records into alerts

Wazuh needs a decoder and rules. Both live in ConfigMaps mounted into the manager.

#### Decoder

Fluent Bit and RFC5424 framing put a byte order mark and an envelope in front of the JSON, which defeats the stock `json` decoder:

```plaintext

1 2026-09-09T18:40:53Z - - - - - <BOM>{"client":"10.0.1.63", ...}

```

```xml

<decoder name="json-bom">

  <prematch type="pcre2">^.\*?(?=\\{\\s\*")</prematch>

  <plugin_decoder offset="after_prematch">JSON_Decoder</plugin_decoder>

</decoder>

```

`type="pcre2"` is required because the default regex engine does not support non-greedy quantifiers. `offset="after_prematch"` is required because JSON\_Decoder otherwise parses the whole raw line and extracts zero fields without complaining.

#### **Rules**

```xml

<group name="bnk,security,gateway">

  <rule id="100100" level="3">

    <decoded_as>json-bom</decoded_as>

    <field name="host">\\.+</field>

    <field name="client">\\.+</field>

    <description>BNK Gateway request logged</description>

    <options>no_full_log</options>

  </rule>

  <rule id="100101" level="5">

    <if_sid>100100</if_sid>

    <action>denied</action>

    <description>BNK Gateway denied request (HTTP 4xx/5xx)</description>

  </rule>

  <rule id="100102" level="8">

    <if_sid>100100</if_sid>

    <action>rate_limited</action>

    <description>BNK Gateway rate-limit triggered</description>

  </rule>

  <rule id="100103" level="12">

    <if_sid>100100</if_sid>

    <field name="method">DELETE</field>

    <description>CRITICAL: DeleteObject or DeleteBucket attempt</description>

  </rule>

</group>

```

#### **The ossec.conf trap**

Mounting decoder and rule files is not enough. If your ConfigMap replaces `ossec.conf`, it must carry an explicit `<ruleset>` block, and that block must also list every built-in Wazuh rule disappears with them:

```xml

<ruleset>

  <decoder_dir>ruleset/decoders</decoder_dir>

  <rule_dir>ruleset/rules</rule_dir>

  <decoder_dir>etc/decoders</decoder_dir>

  <rule_dir>etc/rules</rule_dir>

</ruleset>

```

### **Step 6: See it all in action**

Drive it with real traffic:

```bash

# should raise rule 100100 (level 3)

curl -sk --resolve s3-deptx.org-a.range.local:443:10.0.10.241 \\

  https://s3-deptx.org-a.range.local/minio/health/live

# should raise rule 100103 (level 12)

aws --endpoint-url https://s3-deptx.org-a.range.local --no-verify-ssl \\

  s3 rm s3://dept-x-data/some-object

```

In our run, rules 100100, 100101 and 100103 all fired on live attacker traffic and the alerts index grew from 3 to 55 documents during a single scenario pass.

 ![image](https://d20hrnpixdzcsd.cloudfront.net/original/3X/8/c/8cac5d7605ca63bfcfe104f8a5e935f7bb8b18ce.png)

What BIG-IP Next for Kubernetes adds to the mix

- **An enforcement point that is not the storage product.** Firewall rules and DDoS vectors apply before a request reaches MinIO, and they are Kubernetes objects under version control.

- **Independent telemetry.** The gateway logs what happened regardless of whether the application chose to.

- **Clean ownership split.** Platform team owns Gateway and policy, app teams own routes.

- **Detection you can test.** A `DELETE` produces a level-12 alert correlated by client IP.

## **Summary**

The build is eight objects on top of a running BNK platform:

1. `Gateway` with a VIP and TLS listeners

2. `HTTPRoute` per tenant, owned by the tenant

3. `F5BigFwRulelist` + `F5BigFwPolicy`

4. `F5BigDdosGlobal`

5. `F5BigLogHslpub` + `F5BigLogProfile`

6. `F5BigCneIrule`

7. `SecPolicy` x2, split by target scope

8. `NetPolicy` per listener

Plus a decoder and four rules in Wazuh.

This creates a common point for enforcement and observability that platform and security teams can manage declaratively and under version control.

## Related Content

- [F5 BIG-IP Next for Kubernetes](https://clouddocs.f5.com/bigip-next-for-kubernetes/latest/)
- [BIG-IP Next for Kubernetes | F5](http://f5.com/products/big-ip/next/big-ip-next-for-kubernetes)
