Kubernetes Deployment¶
Deploy the OPNsense Exporter in a Kubernetes cluster with file-based secrets and Prometheus integration.
Prerequisites¶
- A Kubernetes cluster with kubectl access
- OPNsense API credentials (key and secret)
- Optional: Prometheus Operator for automated scrape configuration
Step 1: Create the Secret¶
When you generate API keys on OPNsense, you get a .txt file with the key and secret. Add your OPNsense host and protocol to this file:
Create the Secret in your cluster:
Step 2: Deploy the exporter¶
The following manifest creates a Deployment and a ClusterIP Service. API credentials are mounted as files from the Secret, and connection settings are injected as environment variables.
---
kind: Deployment
apiVersion: apps/v1
metadata:
name: opnsense-exporter
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: opnsense-exporter
template:
metadata:
labels:
app.kubernetes.io/name: opnsense-exporter
spec:
# The exporter is a pure HTTP scrape target and never calls the Kubernetes API,
# so don't mount a ServiceAccount token (shrinks the pivot surface on compromise).
automountServiceAccountToken: false
containers:
- name: opnsense-exporter
# Pin to an immutable release tag, not :latest — :latest is retagged every
# release, so a pod reschedule could silently pull a breaking version with no
# deploy event to correlate. The tag tracks version.txt and is rewritten on
# each release by release-please via the x-release-please-version marker below;
# keep that marker on the image line or the tag stops updating. Published tags
# carry no leading "v" (the git tag is v2.2.1, the image tag is 2.2.1).
image: ghcr.io/rknightion/opnsense-exporter:3.0.0 # x-release-please-version
imagePullPolicy: IfNotPresent
# In pod, mount OPNSense API credentials as files
volumeMounts:
# name must match the volume name below
- name: api-key-vol
mountPath: /etc/opnsense-exporter/creds
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 65532
seccompProfile:
type: RuntimeDefault
ports:
# Default value for --web.listen-address= is 8080
- name: metrics-http
containerPort: 8080
# The syslog receiver (--logs.syslog.enabled), off by default. Both
# protocols are declared because OPNsense can be configured to send over
# either; the exporter listens on both by default. Port 5514, not 514:
# 514 is privileged and this container runs as a non-root user.
- name: syslog-udp
containerPort: 5514
protocol: UDP
- name: syslog-tcp
containerPort: 5514
protocol: TCP
# /-/healthy: process liveness only — no firewall dependency. Do NOT
# point readinessProbe at /-/ready here: when Prometheus scrapes via
# Service endpoints, a not-ready pod stops being scraped and the
# opnsense_up=0 signal is lost exactly when the firewall is down.
livenessProbe:
httpGet:
path: /-/healthy
port: metrics-http
readinessProbe:
httpGet:
path: /-/healthy
port: metrics-http
# See main readme; some configuration options can be set via env-vars
##
args:
- "--log.level=info"
- "--log.format=json"
env:
- name: OPNSENSE_EXPORTER_INSTANCE_LABEL
value: "opnsense"
- name: OPNSENSE_EXPORTER_OPS_API
valueFrom:
secretKeyRef:
name: opnsense-exporter-cfg
key: host
- name: OPNSENSE_EXPORTER_OPS_PROTOCOL
valueFrom:
secretKeyRef:
name: opnsense-exporter-cfg
key: protocol
# Env var points to a location on disk, make sure the value set here matches the volumeMount path
- name: OPS_API_KEY_FILE
value: /etc/opnsense-exporter/creds/api-key
- name: OPS_API_SECRET_FILE
value: /etc/opnsense-exporter/creds/api-secret
# Only enable if using self-signed cert
# - name: OPNSENSE_EXPORTER_OPS_INSECURE
# value: "true"
# in basic testing with a home lab OPNsense 100m CPU and 64Mi memory are sufficient
# however if your opnsense instance has a large number of rules, interfaces, etc...
# you may need to adjust these values
resources:
requests:
memory: 64Mi
cpu: 100m
limits:
memory: 128Mi
cpu: 500m
volumes:
- name: api-key-vol
secret:
secretName: opnsense-exporter-cfg
items:
- key: key
path: api-key
- key: secret
path: api-secret
---
kind: Service
apiVersion: v1
metadata:
name: opnsense-exporter
spec:
selector:
app.kubernetes.io/name: opnsense-exporter
type: ClusterIP
ports:
- name: http
protocol: TCP
port: 8080
targetPort: 8080
# Syslog receiver (--logs.syslog.enabled), off by default. A ClusterIP is only
# reachable from inside the cluster, so the firewall cannot push to it as-is:
# to use the receiver, expose these via a LoadBalancer/NodePort Service (or set
# `type: LoadBalancer` here) and point the OPNsense logging target at that
# address. See docs/syslog-receiver.md.
- name: syslog-udp
protocol: UDP
port: 5514
targetPort: 5514
- name: syslog-tcp
protocol: TCP
port: 5514
targetPort: 5514
Apply the manifest:
Step 3: Configure Prometheus scraping¶
Prometheus Operator (ScrapeConfig)¶
If you are running the Prometheus Operator, create a ScrapeConfig resource:
apiVersion: monitoring.coreos.com/v1alpha1
kind: ScrapeConfig
metadata:
name: opnsense-exporter
labels:
# Match the label selector your Prometheus uses for ScrapeConfig discovery
release: "kube-prom"
spec:
scrapeInterval: 60s
# This bounds only the HTTP snapshot replay. OPNsense API polling runs in the
# background under its own poll intervals and request timeouts, so scrapeTimeout
# no longer controls collector fan-out.
scrapeTimeout: 30s
metricsPath: /metrics
staticConfigs:
- labels:
job: opnsense-exporter
targets:
- opnsense-exporter.default.svc:8080
Prometheus Operator (ServiceMonitor)¶
Alternatively, use a ServiceMonitor:
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: opnsense-exporter
labels:
release: "kube-prom"
spec:
selector:
matchLabels:
app.kubernetes.io/name: opnsense-exporter
endpoints:
- port: http
interval: 30s
path: /metrics
Static Prometheus config¶
If you are not using the Prometheus Operator, add a scrape job to prometheus.yml:
scrape_configs:
- job_name: opnsense
scrape_interval: 30s
static_configs:
- targets:
- opnsense-exporter.default.svc:8080
Verify the deployment¶
kubectl run debug --rm -i --tty --restart=Never --image=alpine - \
wget --quiet -O- opnsense-exporter.default.svc.cluster.local:8080/metrics | head -20
Security considerations¶
The deployment manifest is hardened:
- Read-only root filesystem - no writable paths in the container
- Non-root user - runs as UID 65532
- Dropped capabilities - all Linux capabilities are dropped
- No privilege escalation -
allowPrivilegeEscalation: false - File-based secrets - API credentials are mounted as files, not passed as environment variables
Self-signed certificates
If your OPNsense uses a self-signed certificate, add OPNSENSE_EXPORTER_OPS_INSECURE: "true" to the env section. For production, add the CA certificate to the container's trust store instead.
Restrict access with a NetworkPolicy¶
The /metrics endpoint requires no authentication by default, so restrict which pods can reach it. A sample manifest is provided at deploy/k8s/networkpolicy.yaml:
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: opnsense-exporter
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: opnsense-exporter
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
# Placeholder: the namespace your monitoring stack runs in
kubernetes.io/metadata.name: monitoring
podSelector:
matchLabels:
# Placeholder: the labels on your Prometheus pods
app.kubernetes.io/name: prometheus
ports:
- protocol: TCP
port: 8080
Adjust the namespaceSelector and podSelector placeholders to match where your Prometheus runs, then apply:
Note
NetworkPolicy is only enforced if your cluster's CNI supports it (Calico, Cilium, and similar). On clusters without a policy-capable CNI the manifest is accepted but has no effect.
Disabling collectors¶
Add disable flags to the args array in the Deployment:
args:
- "--log.level=info"
- "--log.format=json"
- "--exporter.disable-cron-table"
- "--exporter.disable-arp-table"
Or use environment variables: