Skip to main content

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

RequirementDetails
OSUbuntu 24.04 LTS or Debian 13 (fresh install)
RAM16 GB minimum, 32 GB recommended
CPU8 cores minimum, 16 recommended
Disk50 GB free space
UserA non-root user with sudo privileges
NetworkInternet 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:

  1. Install Docker from the official Docker repository and enable the service
  2. Add your user to the docker group
  3. Tune kernel inotify limits (required by Kind)
  4. Install Go
  5. Install Kind
  6. Install Helm
  7. Install Helmfile
  8. Install kubectl
  9. 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:

NodeRoleLabels
sfw-dev-cluster-control-planeControl Planeingress-ready=true
sfw-dev-cluster-workerWorkercouchbase=true
sfw-dev-cluster-worker2Workercouchbase=true
sfw-dev-cluster-worker3Workercouchbase=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:

  1. MinIO -- S3-compatible object storage for malware samples and artifacts
  2. cert-manager -- automated TLS certificate management
  3. ingress-nginx -- HTTP/HTTPS ingress controller (runs in hostNetwork mode)
  4. NSQ -- message queue for the processing pipeline
  5. Couchbase Operator -- database operator + single-node Couchbase cluster
  6. Observability -- Prometheus, Grafana, Loki, Tempo, and Alloy
  7. Saferwall -- the application: web UI, API, antivirus engines, and workers
  8. DNS setup -- configures local domains to resolve to the ingress IP
  9. 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

NamespaceComponentsPurpose
minioMinIO (standalone)Object storage for samples, images, artifacts
cert-managercert-managerTLS certificate automation
ingress-nginxNGINX ingress controllerRoutes external traffic to services
nsqnsqd, nsqlookupd, nsqadminMessage queue for async processing
couchbaseCouchbase Operator + ServerDocument database
monitoringPrometheus, Grafana, Loki, Tempo, AlloyMetrics, logs, traces, and dashboards
saferwallWeb UI, API, AV engines, workersThe 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:

ServiceURL
Web UIhttps://saferwall.test
REST APIhttps://api.saferwall.test/v1/
MinIO Consolehttps://minio.saferwall.test

Browser TLS: If your browser still shows a certificate warning, make sure mkcert -install was 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
ConsoleURLCredentials
Couchbasehttp://localhost:8091admin / password
NSQ Adminhttp://localhost:4171No auth
MinIOhttp://localhost:9001minio / minio123
Grafanahttp://localhost:3000admin / 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.mk creates /etc/hosts entries for all three hostnames
  • Ingress -- the saferwall Helm chart uses SAFERWALL_API_DOMAIN for 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 .test or .local TLD 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, and SAFERWALL_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.34 with the actual IP of the VM on your network. Replace the hostnames if you changed SAFERWALL_DOMAIN, SAFERWALL_API_DOMAIN, or SAFERWALL_MINIO_DOMAIN in .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:

  1. 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
  2. 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.crt and run sudo update-ca-certificates.
  3. Restart the browser after installing the CA.

Summary

StepWhatWhere
DNShosts file entries pointing all three domains to VM IPClient machine
TLS trust (opt.)Install mkcert root CA in the system/browser trust storeClient 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:

  1. Web API receives the file, stores it in MinIO, and publishes a message to NSQ
  2. Static analysis worker picks up the message, performs static analysis, publishes results
  3. AV scan engines (ClamAV, Avira, Comodo, Windows Defender) each scan the file via gRPC
  4. Aggregator collects all scan results and writes them to Couchbase
  5. Post-processor performs final enrichment and cleanup

All services communicate asynchronously through NSQ topics and store results in Couchbase.

Next Steps