Test Deployment
This guide walks you through deploying a fully functional Saferwall instance on a local Kind (Kubernetes in Docker) cluster. This is the recommended path for evaluating the platform before committing to a production deployment.
By the end of this guide you will have:
- A 4-node Kubernetes cluster running inside Docker containers
- The complete Saferwall stack: web UI, REST API, antivirus engines, and processing pipeline
- A full observability stack: Prometheus, Grafana, Loki, Tempo, and Alloy
- Trusted HTTPS on local domains
- The ability to create an account, upload a file, and see scan results
Prerequisites
| Requirement | Details |
|---|---|
| OS | Ubuntu 24.04 LTS or Debian 13 (fresh install) |
| RAM | 16 GB minimum, 32 GB recommended |
| CPU | 8 cores minimum, 16 recommended |
| Disk | 50 GB free space |
| User | A non-root user with sudo privileges |
| Network | Internet access (to pull container images and Helm charts) |
Note: The Kind cluster runs 4 Docker containers (1 control-plane + 3 workers) and deploys around 20 pods. Resource-constrained machines may experience slow startups or pod evictions.
1. Extract the Distribution Archive
Extract the saferwall-kind-dev.zip archive you received and enter the directory:
unzip saferwall-kind-dev.zip
cd saferwall-kind-dev
2. Install All Prerequisites
A single make target installs everything you need: Docker, Go, Kind, kubectl, Helm, Helmfile, and mkcert.
make kind/setup/ubuntu
This command will:
- Install Docker from the official Docker repository and enable the service
- Add your user to the
dockergroup - Tune kernel inotify limits (required by Kind)
- Install Go
- Install Kind
- Install Helm
- Install Helmfile
- Install kubectl
- Install mkcert (local CA for trusted TLS)
Important: After this step, log out and log back in so Docker group membership takes effect:
# Log out
exit
# Log back in, then verify Docker works without sudo:
docker ps
3. Create the Kind Cluster
make kind/cluster/create
This creates a cluster named sfw-dev-cluster with the following topology:
| Node | Role | Labels |
|---|---|---|
| sfw-dev-cluster-control-plane | Control Plane | ingress-ready=true |
| sfw-dev-cluster-worker | Worker | couchbase=true |
| sfw-dev-cluster-worker2 | Worker | couchbase=true |
| sfw-dev-cluster-worker3 | Worker | couchbase=true |
All nodes run Kubernetes v1.33.4.
Verify the cluster is running:
kubectl cluster-info --context kind-sfw-dev-cluster
kubectl get nodes
You should see all 4 nodes in Ready status.
4. Deploy Saferwall
Deploy the entire stack with a single command:
make kind/deploy-app
This runs through the following stages in order:
- MinIO -- S3-compatible object storage for malware samples and artifacts
- cert-manager -- automated TLS certificate management
- ingress-nginx -- HTTP/HTTPS ingress controller (runs in hostNetwork mode)
- NSQ -- message queue for the processing pipeline
- Couchbase Operator -- database operator + single-node Couchbase cluster
- Observability -- Prometheus, Grafana, Loki, Tempo, and Alloy
- Saferwall -- the application: web UI, API, antivirus engines, and workers
- DNS setup -- configures local domains to resolve to the ingress IP
- TLS setup -- creates a local CA and loads it into Kubernetes for trusted HTTPS
This step takes 10-20 minutes depending on your internet speed and machine resources. Most of the time is spent pulling container images.
What Gets Deployed
| Namespace | Components | Purpose |
|---|---|---|
minio | MinIO (standalone) | Object storage for samples, images, artifacts |
cert-manager | cert-manager | TLS certificate automation |
ingress-nginx | NGINX ingress controller | Routes external traffic to services |
nsq | nsqd, nsqlookupd, nsqadmin | Message queue for async processing |
couchbase | Couchbase Operator + Server | Document database |
monitoring | Prometheus, Grafana, Loki, Tempo, Alloy | Metrics, logs, traces, and dashboards |
saferwall | Web UI, API, AV engines, workers | The application itself |
5. Wait for All Pods to Be Ready
After make kind/deploy-app completes, verify that all pods are running:
kubectl get pods -A
Some pods (especially in the saferwall namespace) may take a few extra minutes to
pull their images and start. Wait until all pods show Running or Completed status.
You can watch a specific namespace:
# Watch saferwall pods until they are all ready
kubectl -n saferwall get pods -w
To wait for all saferwall pods to be ready (with a 15-minute timeout):
kubectl -n saferwall wait --for=condition=Ready pod --all --timeout=900s
6. Verify DNS and TLS
The make kind/deploy-app command already configured DNS and TLS for you. Verify it:
# Check DNS resolution
getent hosts saferwall.test
getent hosts api.saferwall.test
# Should both resolve to the ingress IP
If DNS is not resolving, you can re-run the setup:
make dns/setup
make tls/mkcert/setup
How DNS Works
On Ubuntu/Debian with systemd-resolved (the default), entries are added to /etc/hosts
pointing all three hostnames (SAFERWALL_DOMAIN, SAFERWALL_API_DOMAIN,
SAFERWALL_MINIO_DOMAIN) to the Kind control-plane node IP. The ingress-nginx controller
runs in hostNetwork mode on the control-plane node, so ports 80 and 443 are served
directly from the node. Kind's extraPortMappings then publish these ports on all host
interfaces (0.0.0.0), making them accessible from outside the machine.
How TLS Works
mkcert creates a local Certificate Authority (CA) and installs it in the system trust
store. The CA's key pair is loaded into Kubernetes as a Secret, and cert-manager uses it
to issue TLS certificates for all ingress resources automatically. This gives you trusted
HTTPS without browser warnings.
7. Access the Application
Open your browser and navigate to:
| Service | URL |
|---|---|
| Web UI | https://saferwall.test |
| REST API | https://api.saferwall.test/v1/ |
| MinIO Console | https://minio.saferwall.test |
Browser TLS: If your browser still shows a certificate warning, make sure
mkcert -installwas run successfully. You may need to restart the browser after the CA was installed.
8. Basic Tests
8.1 Create a User Account
Using curl:
curl -sk -X POST https://api.saferwall.test/v1/users/ \
-H "Content-Type: application/json" \
-d '{"username": "testuser", "password": "TestPass1234!", "email": "test@saferwall.test"}'
Or using httpie (install with sudo apt install httpie):
http --verify=no POST https://api.saferwall.test/v1/users/ \
username=testuser password=TestPass1234! email=test@saferwall.test
8.2 Make a User Admin
To grant admin privileges, update the user document directly in Couchbase:
kubectl exec -n couchbase saferwall-cb-cluster-0000 -c couchbase-server -- \
cbq -u admin -p password \
-s "UPDATE \`sfw\` SET admin = true WHERE username = 'testuser' AND type = 'user' RETURNING username, admin;"
8.3 Log In and Get a Token
TOKEN=$(curl -sk -X POST https://api.saferwall.test/v1/auth/login/ \
-H "Content-Type: application/json" \
-d '{"username": "testuser", "password": "TestPass1234!"}' | jq -r '.token')
echo "Token: $TOKEN"
8.4 Scan a File (EICAR Test File)
The EICAR test file is a harmless file that all antivirus engines detect as malware. It is the standard way to verify that AV scanning is working.
Create the EICAR test file and upload it:
# Create the EICAR test file
echo 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > /tmp/eicar.txt
# Upload it for scanning
curl -sk -X POST https://api.saferwall.test/v1/files/ \
-H "Authorization: Bearer ${TOKEN}" \
-F "file=@/tmp/eicar.txt"
The response will include a sha256 hash. Use it to check the scan results:
# Replace <sha256> with the hash from the upload response
curl -sk https://api.saferwall.test/v1/files/<sha256> | jq .
Note: Scanning is asynchronous. The file goes through multiple processing stages (static analysis, AV scanning, aggregation, post-processing). It may take 1-2 minutes for all results to appear. Poll the endpoint or check the web UI.
You can also upload the file through the web UI at https://saferwall.test -- create an account or log in, then drag and drop a file onto the upload area.
8.5 Verify Antivirus Results
The dev cluster enables 2 antivirus engines by default: ClamAV and Windows Defender.
After the scan completes, you should see detection results from these engines in the
API response under the multiav field.
# Check multiav results for the EICAR file
curl -sk https://api.saferwall.test/v1/files/<sha256> | jq '.multiav'
9. Admin Consoles (Port Forwarding)
For debugging and administration, you can access internal services via port forwarding. Each command blocks the terminal, so open separate terminal sessions:
# Couchbase admin console -- http://localhost:8091 (admin / password)
make k8s/couchbase/pf
# NSQ admin console -- http://localhost:4171
make k8s/nsq/pf
# MinIO admin console -- http://localhost:9001 (minio / minio123)
make k8s/minio/pf
# Grafana dashboard -- http://localhost:3000 (admin / prom-operator)
make k8s/grafana/pf
| Console | URL | Credentials |
|---|---|---|
| Couchbase | http://localhost:8091 | admin / password |
| NSQ Admin | http://localhost:4171 | No auth |
| MinIO | http://localhost:9001 | minio / minio123 |
| Grafana | http://localhost:3000 | admin / prom-operator |
Observability Stack
The dev cluster includes a full observability stack:
- Grafana -- dashboards and visualization
- Prometheus -- metrics collection and alerting
- Loki -- log aggregation (query logs from the saferwall namespace)
- Tempo -- distributed tracing (traces from the API and workers)
- Alloy -- DaemonSet agent that collects logs and traces from saferwall pods
Access Grafana via port-forward and explore:
- Logs: Go to Explore > select Loki datasource > query
{namespace="saferwall"} - Traces: Go to Explore > select Tempo datasource > search for traces
10. Stopping and Starting the Cluster
Stop (preserve state)
This stops the Docker containers without deleting the cluster. Your data and configuration are preserved.
make kind/cluster/stop
Start (resume)
make kind/cluster/start
After restarting, it may take a minute for all pods to become ready. If DNS stops working, re-run
make dns/setup.
Delete (destroy everything)
make kind/cluster/delete
This deletes the cluster, all data, and removes the DNS configuration. To start fresh, go back to step 3.
11. Full One-Command Deployment
If you want to do everything in one shot (delete any existing cluster and deploy from scratch):
make kind/up
This is equivalent to:
make kind/cluster/delete || true
make kind/cluster/create
make kind/deploy-app # includes DNS and TLS setup
Customizing the Deployment
All configuration lives in helmfile.d/environments/dev.yaml.gotmpl. Edit this file before
running make kind/deploy-app, or edit it afterward and redeploy the affected component.
Toggling Components
Every major component can be enabled or disabled independently. Set enabled: true or
enabled: false in helmfile.d/environments/dev.yaml.gotmpl under the saferwall: section:
# helmfile.d/environments/dev.yaml.gotmpl
saferwall:
ui:
enabled: true # Web frontend (disable for API-only usage)
multiav:
enabled: true # Multi-AV scanning subsystem (see below)
After changing values, redeploy the saferwall component:
helmfile sync -e dev -l component=saferwall
Using a Custom Domain Name
By default, the deployment uses saferwall.test as the domain with api.saferwall.test
and minio.saferwall.test as subdomains. All three hostnames are independently
configurable via the .env file, so you are not forced to use subdomains.
To use custom domains, edit .env:
# .env
SAFERWALL_DOMAIN = malware.lab
SAFERWALL_API_DOMAIN = malware-api.lab
SAFERWALL_MINIO_DOMAIN = malware-storage.lab
These variables propagate to:
- Helmfile -- the dev environment values (
dev.yaml.gotmpl) use{{ requiredEnv "SAFERWALL_DOMAIN" }},{{ requiredEnv "SAFERWALL_API_DOMAIN" }}, and{{ requiredEnv "SAFERWALL_MINIO_DOMAIN" }}to template the ingress hosts, the saferwall chart hostnames, and the UI environment variables - DNS setup --
mk/dns.mkcreates/etc/hostsentries for all three hostnames - Ingress -- the saferwall Helm chart uses
SAFERWALL_API_DOMAINfor the API ingress host (no subdomain prefix assumed)
After changing the domain, redeploy and reconfigure DNS/TLS:
helmfile sync -e dev -l component=minio
helmfile sync -e dev -l component=saferwall
make dns/setup
make tls/mkcert/setup
Or, if starting fresh, just run make kind/up -- it will pick up the new domain
from .env automatically.
Domain choice tips:
- Use a
.testor.localTLD to avoid conflicts with real domains- Avoid
.dev-- browsers force HTTPS via HSTS preload, which can cause issues- If other machines on your LAN need access, make sure the domain doesn't conflict with your internal DNS
Using Your Own TLS Certificate
By default, the deployment uses mkcert to generate a local CA and cert-manager to
automatically issue TLS certificates. If you already have your own TLS certificate
(e.g., from your organization's internal CA or a commercial provider), you can use it
instead.
Set three variables in .env:
# .env
SAFERWALL_TLS_ISSUER = none
SAFERWALL_TLS_CERT = /path/to/your/cert.pem
SAFERWALL_TLS_KEY = /path/to/your/key.pem
Then deploy as usual:
make kind/up
The make tls/setup target (called automatically by make kind/deploy-app) detects
SAFERWALL_TLS_ISSUER=none and creates the TLS secrets from your cert/key files in
both the saferwall and minio namespaces. The cert-manager.io/cluster-issuer
annotation is omitted from all ingress resources, so cert-manager will not overwrite
your secrets.
Note: Ensure your certificate covers all three hostnames configured in
.env:SAFERWALL_DOMAIN,SAFERWALL_API_DOMAIN, andSAFERWALL_MINIO_DOMAIN. If they share a common parent domain, a wildcard cert works. Otherwise, use a SAN certificate.
To switch back to auto-generated certificates, set SAFERWALL_TLS_ISSUER = mkcert-ca
and clear the cert/key paths. Then redeploy.
Accessing Saferwall from the LAN
If you deployed Saferwall inside a VM and want to access it from the host machine or
other devices on the LAN, this works out of the box thanks to Kind's
extraPortMappings. The Kind cluster config binds ports 80 and 443 on 0.0.0.0, so
Docker publishes them on all VM network interfaces automatically. No iptables rules, no
IP forwarding, no extra processes needed.
Network Overview
LAN Client VM (e.g., 10.250.125.34) Kind Cluster
────────── ────────────────────── ────────────
Browser Docker control-plane node
│ │ │
│ SAFERWALL_DOMAIN │ extraPortMappings │ ingress-nginx
│ ─────────────> DNS ──> VM IP ─┤ :80 (0.0.0.0) │ (hostNetwork)
│ │ :443 (0.0.0.0) ├─ SAFERWALL_DOMAIN -> ui
│ │ ├─ SAFERWALL_API_DOMAIN -> api
│ <──────── HTTPS ────────────────┤─────────────────────────────────> ├─ SAFERWALL_MINIO_DOMAIN -> minio
│ │ │
Step 1: Configure DNS on the Client Machine
The client machine needs to resolve saferwall.test to the VM's IP address.
Add entries for all three hostnames from your .env file. For example, with the
defaults:
Windows -- edit C:\Windows\System32\drivers\etc\hosts as Administrator:
10.250.125.34 saferwall.test
10.250.125.34 api.saferwall.test
10.250.125.34 minio.saferwall.test
macOS / Linux -- edit /etc/hosts:
10.250.125.34 saferwall.test
10.250.125.34 api.saferwall.test
10.250.125.34 minio.saferwall.test
Tip: Replace
10.250.125.34with the actual IP of the VM on your network. Replace the hostnames if you changedSAFERWALL_DOMAIN,SAFERWALL_API_DOMAIN, orSAFERWALL_MINIO_DOMAINin.env.
Step 2: Trust the TLS Certificate (Optional)
Without this step, the browser will show a "Not secure" warning. To get a trusted green lock:
-
Copy the mkcert CA certificate from the VM to the client machine. The CA cert is located at:
# On the VM, find the CA cert location
mkcert -CAROOT
# Typically: /home/<user>/.local/share/mkcert/rootCA.pem -
Install the CA on the client:
- Windows: Double-click
rootCA.pem> "Install Certificate" > "Local Machine""Place all certificates in the following store" > "Trusted Root Certification Authorities" > Finish.
- macOS: Double-click
rootCA.pem> it opens in Keychain Access > select "System" keychain > mark the certificate as "Always Trust". - Linux: Copy to
/usr/local/share/ca-certificates/mkcert-ca.crtand runsudo update-ca-certificates.
- Windows: Double-click
-
Restart the browser after installing the CA.
Summary
| Step | What | Where |
|---|---|---|
| DNS | hosts file entries pointing all three domains to VM IP | Client machine |
| TLS trust (opt.) | Install mkcert root CA in the system/browser trust store | Client machine |
Troubleshooting
Pods stuck in Pending or ImagePullBackOff
Check available resources and pod events:
kubectl describe node | grep -A5 "Allocated resources"
kubectl -n saferwall describe pod <pod-name>
Common causes:
- Insufficient memory: Kind nodes share the host's resources. Close other applications or increase host RAM.
- Disk full: Container images are large. Free up disk space.
- Network issues: Image pulls may fail. Check your internet connection and retry.
DNS not resolving
# Check current DNS config
make dns/status
# Re-apply DNS configuration
make dns/setup
# Verify
getent hosts saferwall.test
TLS certificate warnings in browser
# Re-install the local CA
mkcert -install
# Reload the CA into Kubernetes
make tls/mkcert/ca-load
# Restart the browser
Couchbase pod not starting
The Couchbase operator needs to be fully running before the Couchbase server pod can be created. Check the operator status:
kubectl -n couchbase get pods
kubectl -n couchbase wait --for=condition=Available deployment --all --timeout=120s
If the server pod exists but is not ready:
kubectl -n couchbase describe pod -l app=couchbase
kubectl -n couchbase logs -l app=couchbase --tail=50 --all-containers
Redeploying a single component
You can redeploy individual components without redeploying everything:
# Redeploy just the saferwall application
helmfile sync -e dev -l component=saferwall
# Redeploy just the ingress controller
helmfile sync -e dev -l component=ingress-controller
# Redeploy just MinIO
helmfile sync -e dev -l component=minio
Checking logs
# API server logs
kubectl -n saferwall logs -l app.kubernetes.io/component=webapis --tail=100
# AV engine logs (e.g., ClamAV)
kubectl -n saferwall logs -l app.kubernetes.io/component=clamav --tail=100
# All saferwall pods
kubectl -n saferwall logs -l app.kubernetes.io/instance=saferwall --tail=50 --all-containers
Architecture Overview
┌──────────────────────────────────────────────────────────┐
│ Kind Cluster │
Browser │ │
│ │ ┌─────────────┐ ┌──────────┐ ┌──────────────┐ │
│ HTTPS │ │ingress-nginx│────▶│ Web UI │ │ Couchbase │ │
├───────────────▶│ │(hostNetwork)│────▶│ Web APIs │────▶│ (database) │ │
│ │ └─────────────┘ └──────────┘ └──────────────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌─────────┐ ┌──────────────┐ │
│ │ │ MinIO │ │ NSQ │ │
│ │ │(storage)│ │ (queue) │ │
│ │ └─────────┘ └──────┬───────┘ │
│ │ │ │
│ │ ┌───────────────────────────────────┤ │
│ │ ▼ ▼ ▼ ▼ │
│ │ ┌───────────┐ ┌──────────┐ ┌─────────┐ ┌──────────┐ │
│ │ │ Static │ │ AV Scan │ │Aggregat.│ │PostProc. │ │
│ │ │ Analysis │ │ Engines │ │ │ │ │ │
│ │ └───────────┘ └──────────┘ └─────────┘ └──────────┘ │
│ │ │
│ │ ┌───────────────────────────────────────────────────┐ │
│ │ │ Observability: Prometheus + Grafana + Loki + Tempo│ │
│ │ └───────────────────────────────────────────────────┘ │
│ └──────────────────────────────────────────────────────────┘
│
│ DNS: configured domains ──▶ Kind node IP (/etc/hosts)
│ TLS: mkcert local CA ──▶ cert-manager ──▶ trusted HTTPS
│ Ports 80/443 exposed via extraPortMappings (0.0.0.0)
Processing Pipeline
When a file is uploaded:
- Web API receives the file, stores it in MinIO, and publishes a message to NSQ
- Static analysis worker picks up the message, performs static analysis, publishes results
- AV scan engines (ClamAV, Avira, Comodo, Windows Defender) each scan the file via gRPC
- Aggregator collects all scan results and writes them to Couchbase
- Post-processor performs final enrichment and cleanup
All services communicate asynchronously through NSQ topics and store results in Couchbase.
Next Steps
- Explore the web UI at https://saferwall.test
- Check out the REST API documentation
- Access Grafana for monitoring and log exploration
- For production deployments on bare-metal servers, see the RKE2 deployment guide