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::senddoes not prepend a syslog priority header even when the publisher declaresformat: rfc5424, andwazuh-remoteddiscards 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::openreturns 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
DELETEproduces a level-12 alert correlated by client IP.
Summary
The build is eight objects on top of a running BNK platform:
-
Gatewaywith a VIP and TLS listeners -
HTTPRouteper tenant, owned by the tenant -
F5BigFwRulelist+F5BigFwPolicy -
F5BigDdosGlobal -
F5BigLogHslpub+F5BigLogProfile -
F5BigCneIrule -
SecPolicyx2, split by target scope -
NetPolicyper 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.



