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
- Minecraft Hardening completed
What We're Hardening
| Component | Before | After |
|---|---|---|
| Storage | Unencrypted longhorn | Encrypted longhorn-encrypted |
| NetworkPolicy | None | Egress limited |
| Helm chart | Floating version | Pinned 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.
| Important | Copy 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
Longhorn, "Volume Encryption," longhorn.io. Accessed: Feb. 28, 2026. [Online]. Available: https://longhorn.io/docs/1.9.2/advanced-resources/security/volume-encryption/ ↩
Longhorn, "Encrypt existing volume," github.com. Accessed: Feb. 28, 2026. [Online]. Available: https://github.com/longhorn/longhorn/issues/9502 ↩