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

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

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,

~/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.


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:


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


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


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:


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.

The publisher


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

~/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


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


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:


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


<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


<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:


<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:


# 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.

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

1 Like