𝔩𝔢𝔩𝕠𝔭𝔢𝔷
Theme

Homelab

Factorio Hardening

Hardening Factorio: Encrypted Storage, NetworkPolicy, and Security

Overview

This article hardens the Factorio server by migrating to encrypted storage, adding NetworkPolicy, and pinning Helm versions. Game servers are internet-exposed via playit.gg - hardening limits the blast radius if compromised.

Tip:Having trouble? See v1.9.0 for what your setup should look like after completing this article.

Before You Begin

Prerequisites

What We're Hardening

ComponentBeforeAfter
StorageUnencrypted longhornEncrypted longhorn-encrypted
NetworkPolicyNoneEgress limited
Helm chartFloating versionPinned version

Why These Controls

Encrypted Storage: Save data protected at rest. Physical disk theft or node compromise no longer exposes plaintext data.

NetworkPolicy: Factorio needs to reach the internet for playit.gg tunnel. We can still block access to other VLANs (Network, Drive).

Helm Pinning: Ensures reproducible deployments without surprise breaking changes.

How Longhorn Encryption Works

Longhorn uses LUKS (Linux Unified Key Setup) for block-level encryption1. You cannot convert existing unencrypted volumes - the block format differs. Instead, we backup the data, create a new encrypted volume, and restore2.

Backup Save Data

Create a tar backup of the entire data directory (/factorio mount).

# Get pod name
POD=$(kubectl get pod -n factorio -l app=factorio-factorio-server-charts -o jsonpath='{.items[0].metadata.name}')

# Create backup inside container
kubectl exec -n factorio $POD -- tar czf /tmp/factorio-backup.tar.gz -C /factorio .

# Copy to local machine
kubectl cp factorio/$POD:/tmp/factorio-backup.tar.gz ./factorio-backup-$(date +%Y%m%d).tar.gz

Verify the backup:

# Check file exists and size
ls -lh ./factorio-backup-$(date +%Y%m%d).tar.gz

# List contents
tar tzf ./factorio-backup-$(date +%Y%m%d).tar.gz | head -20

Expected: File is several MB, contents show saves/, config/, etc.

Harden Factorio

Check Current Version

# Find current chart version to pin
helm list -n factorio

Note the chart version (e.g., factorio-server-charts-2.5.2) - this is what you'll pin.

PVC

Update to use the encrypted volume.

k8s/apps/factorio/pvc.yaml:

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
    name: factorio-data-encrypted # CHANGE from factorio-data
    namespace: factorio
spec:
    accessModes:
        - ReadWriteOnce
    storageClassName: longhorn-encrypted # CHANGE from longhorn
    resources:
        requests:
            storage: 10Gi

HelmRelease

k8s/apps/factorio/helmrelease.yaml:

# ... existing HelmRepository ...
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
    name: factorio
    namespace: factorio
spec:
    interval: 30m
    chart:
        spec:
            chart: factorio-server-charts
            version: "2.5.2" # PIN to current version
            sourceRef:
                kind: HelmRepository
                name: factorio
    # ... existing install, upgrade ...
    values:
        # ... existing values ...
        persistence:
            enabled: true
            dataDir:
                existingClaim: factorio-data-encrypted # CHANGE from factorio-data

NetworkPolicy

k8s/apps/factorio/networkpolicy.yaml:

---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
    name: factorio
    namespace: factorio
spec:
    podSelector: {}
    policyTypes:
        - Egress
    egress:
        # DNS resolution
        - to:
              - namespaceSelector:
                    matchLabels:
                        kubernetes.io/metadata.name: kube-system
          ports:
              - protocol: UDP
                port: 53
        # Internet (playit.gg)
        # Block private ranges
        - to:
              - ipBlock:
                    cidr: 0.0.0.0/0
                    except:
                        - 10.0.0.0/8
                        - 172.16.0.0/12
                        - 192.168.0.0/16

Kustomization

k8s/apps/factorio/kustomization.yaml:

# ... existing header ...
resources:
    - namespace.yaml
    - pvc.yaml
    - secret.sops.yaml
    - helmrelease.yaml
    - networkpolicy.yaml # ADD

Deploy Changes

Commit Changes

git add k8s/apps/factorio/
git commit -m "feat(factorio): harden with encrypted storage and NetworkPolicy"
git push

Reconcile

flux reconcile source git flux-system
flux reconcile kustomization sync

Factorio will start with an empty encrypted volume.

Restore Save

Extract just the save file from the backup and copy to the PVC. The server generates other files (configs, mods) on startup - only the save needs restoring.

ImportantCopy the save while Factorio is stopped. If copied while running, Factorio autosaves on shutdown and overwrites your restored save.

Extract Save

# List saves in backup to find your save filename
tar tzvf ./factorio-backup-*.tar.gz | grep saves/

Note the save filename that matches your save_name in the HelmRelease (7MB+ for an established world).

# Set your save filename (used throughout restore)
SAVE_FILE="<save-file.zip>"

# Extract just the save file
tar xzf ./factorio-backup-*.tar.gz ./saves/$SAVE_FILE
mv ./saves/$SAVE_FILE ./
rmdir saves

Scale Down

Stop Factorio so it won't autosave over our restored data:

kubectl scale deploy -n factorio factorio-factorio-server-charts --replicas=0
kubectl wait --for=delete pod -n factorio -l app=factorio-factorio-server-charts --timeout=120s

Copy Save to PVC

Use a temporary pod to copy the save while no server is running:

# Create pod with PVC mounted
kubectl run -n factorio copy-save --restart=Never \
  --image=busybox \
  --overrides='{
    "spec": {
      "containers": [{
        "name": "copy-save",
        "image": "busybox",
        "command": ["sleep", "300"],
        "volumeMounts": [{"name": "data", "mountPath": "/factorio"}]
      }],
      "volumes": [{
        "name": "data",
        "persistentVolumeClaim": {"claimName": "factorio-data-encrypted"}
      }]
    }
  }'

kubectl wait --for=condition=ready pod -n factorio copy-save --timeout=60s

# Clear saves folder and copy save file
kubectl exec -n factorio copy-save -- sh -c 'rm -rf /factorio/saves/*'
kubectl cp ./$SAVE_FILE factorio/copy-save:/factorio/saves/$SAVE_FILE

# Verify size matches backup (7MB+ for established world)
kubectl exec -n factorio copy-save -- ls -la /factorio/saves/

# Clean up
kubectl delete pod -n factorio copy-save

Scale Up

kubectl scale deploy -n factorio factorio-factorio-server-charts --replicas=1

Verify Hardening

Verify Pod Running

kubectl get pods -n factorio

Expected: Pod shows Running with all containers ready.

Verify Encrypted PVC

Confirm the pod is using the encrypted volume:

kubectl get pvc -n factorio

Expected: factorio-data-encrypted shows Bound.

Verify NetworkPolicy

kubectl get networkpolicy -n factorio

Expected: factorio policy listed.

Verify Save Loads

Check logs for the correct save being loaded:

kubectl logs -n factorio deploy/factorio-factorio-server-charts -c factorio-factorio-server-charts | grep "Loading map"

Expected:

Loading map /factorio/saves/<your-save-name>.zip: XXXXXX bytes.

The byte count should match your save file size (several MB for an established world). A small file (~900KB) indicates a fresh world was generated instead.

Clean Up Old Volume

After verifying Factorio works:

kubectl delete pvc -n factorio factorio-data

Next Steps

With Factorio hardened, continue with Plex hardening.

See: Plex Hardening

Resources

Footnotes

  1. Longhorn, "Volume Encryption," longhorn.io. Accessed: Feb. 28, 2026. [Online]. Available: https://longhorn.io/docs/1.9.2/advanced-resources/security/volume-encryption/

  2. Longhorn, "Encrypt existing volume," github.com. Accessed: Feb. 28, 2026. [Online]. Available: https://github.com/longhorn/longhorn/issues/9502

Previous
Minecraft Hardening