Encryption at rest#
MagStacker holds sensitive inventory — serial numbers, and (with document
attachments) receipts, warranties, and ATF forms. This page covers the two
layers MagStacker uses to keep that data safe when it's not sitting in a live,
authenticated session, and draws a clear line around what each layer does and
doesn't defend against.
The two layers:
- Encrypted backups — an admin-run, password-encrypted export of the whole
instance (database + document blobs), made from the Admin → Backup
screen. This is application-level: MagStacker does the encrypting. - Encrypted host volumes — the disk(s) holding Postgres's data and the
document upload directory (UPLOAD_DIR) are encrypted at the storage layer.
This is host-level, and it is your responsibility, not the app's.
MagStacker cannot encrypt the disk it's running on; nothing in the app can
reach below the filesystem it's handed.
Both matter, and they cover different threats — see the threat-coverage
matrix below.
Threat-coverage matrix#
| Threat | Scenario | Covered by | Not covered by |
|---|---|---|---|
| Seized or stolen disk | A drive is taken from a powered-off or decommissioned box — theft, hardware disposal, or state seizure. | Encrypted host volume (LUKS or an encrypted cloud volume) | Encrypted backups don't help here — they protect a bundle in transit or at rest off-box, not the live disk itself. |
| Leaked or exfiltrated backup bundle | A downloaded backup file ends up somewhere it shouldn't — emailed, left on a laptop, uploaded to the wrong place. | Encrypted backup (password-derived key, authenticated encryption; see the no-recovery caveat below) | An encrypted host volume doesn't help here — once the bundle leaves the box, disk encryption is irrelevant to it. |
| Compromised running host | An attacker gets code execution or root on the live, running server while it's up and the disk is unlocked. | Neither layer. Explicitly out of scope. | Both an unlocked encrypted volume and a decrypted-in-memory backup password are available to a live attacker with host access; disk encryption and backup encryption both assume an offline or exfiltrated artifact, not a compromised live process. |
If you need to defend against a compromised running host specifically, that's
a different problem — hardening the host OS, minimizing attack surface,
network segmentation, and so on — and it's out of scope for this page. (For
what it's worth: this is also why MagStacker doesn't encrypt individual
database columns. An app-held key on a compromised host wouldn't meaningfully
help this threat, and it would break the server-side querying — serial-number
lookup and dedup — the product depends on.)
Host responsibility: encrypting the volumes#
This is a host-level task you own — MagStacker ships a deploy posture
that makes it a low-friction path, but it does not and cannot perform the
encryption itself. There are two volumes to place on encrypted storage, both
defined in docker-compose.yml:
magstacker-pgdata— the Postgres data directory (all inventory, users,
grants, and firearm records).magstacker-uploads— theUPLOAD_DIRmount (every document blob:
receipts, warranties, ATF forms).
Docker's default local volume driver stores named volumes under
/var/lib/docker/volumes/ on the host. That means the simplest way to
encrypt both at once is to encrypt the storage backing Docker's data root.
If you want to encrypt only these two volumes (leaving the rest of
/var/lib/docker alone), point them at a dedicated encrypted mount instead.
Both approaches are below.
Transient restore data lives here too. A force-replace restore (see
Backups and the no-recovery caveat)
briefly writes a plaintext snapshot of the outgoing database into a temp
schema inside Postgres's data directory, and moves the outgoing
UPLOAD_DIRcontents aside on the same upload volume, before wiping and
re-applying. Both live on the volumes covered by the guidance below, so
encryptingmagstacker-pgdataandmagstacker-uploadsalso covers that
transient window — there's nothing extra to configure for it.
Option A — encrypt the whole Docker data root (simplest)#
Do this at OS-install time, or by relocating Docker's data root onto an
already-encrypted disk. Because every named volume lives under
/var/lib/docker/volumes/, this transparently covers magstacker-pgdata and
magstacker-uploads (and any future volumes) with no compose changes.
Self-hosted, with LUKS (Linux):
-
Identify the disk or partition you'll dedicate to Docker (e.g.
/dev/sdb1)
and encrypt it:sudo cryptsetup luksFormat /dev/sdb1 sudo cryptsetup open /dev/sdb1 docker_data sudo mkfs.ext4 /dev/mapper/docker_data -
Mount it where Docker expects its data root:
sudo mkdir -p /var/lib/docker sudo mount /dev/mapper/docker_data /var/lib/docker(If Docker was already initialized on this host, stop it first, move the
existing/var/lib/dockercontents onto the new encrypted filesystem,
then remount.) -
Add the mapping to
/etc/crypttabso the volume can be unlocked on boot,
and the mount to/etc/fstab. LUKS needs a passphrase or keyfile supplied
at unlock time — decide up front whether that's a manual passphrase entry
at boot (safest, but means the box doesn't come back up unattended after
a power cycle) or a keyfile (more automated, but the keyfile itself must
not live unencrypted next to the volume it unlocks — keep it on separate
protected storage, e.g. a TPM-backed unlock or a secrets manager your boot
process can reach). -
docker compose up -das normal — Docker now writesmagstacker-pgdata
andmagstacker-uploadsonto the encrypted filesystem without any compose
changes.
Cloud, with an encrypted volume:
The exact steps vary by provider, but the shape is the same everywhere:
- Create (or attach) a block volume with encryption enabled at creation —
e.g. AWS EBS--encrypted(or turn on the account-level "always encrypt
new EBS volumes" default), a GCP persistent disk (encrypted by default, or
with a customer-managed key for stricter control), an Azure managed disk
(server-side encryption is on by default; add a customer-managed key if
you need to hold your own key), or your provider's equivalent. - Attach it to the instance and mount it at the path that will back Docker's
data root (or relocate Docker'sdata-rootto it via
/etc/docker/daemon.json:{ "data-root": "/mnt/encrypted-docker" },
then restart the Docker daemon). docker compose up -das normal.
Option B — encrypt just the MagStacker volumes (targeted)#
If you'd rather not touch Docker's whole data root, mount an encrypted
filesystem at a dedicated path and repoint only magstacker-pgdata and
magstacker-uploads at it using compose's bind-style driver_opts. Set up
the encrypted mount the same way as Option A (LUKS steps 1–3, or an attached
encrypted cloud volume), mounted at, say, /mnt/magstacker-encrypted, then
override the two volumes:
# docker-compose.override.yml
volumes:
magstacker-pgdata:
driver_opts:
type: none
o: bind
device: /mnt/magstacker-encrypted/pgdata
magstacker-uploads:
driver_opts:
type: none
o: bind
device: /mnt/magstacker-encrypted/uploads
Create the two subdirectories (pgdata, uploads) on the encrypted mount
before first up so Docker has somewhere to bind to. docker compose up -d
then reads the override automatically alongside docker-compose.yml.
Backups and the no-recovery caveat#
The other layer — encrypted backups — is covered from the Admin → Backup
screen in the app: an admin sets a password, and MagStacker exports the
entire instance (database plus every document blob) as a single
authenticated-encrypted file, downloaded on demand. Restoring reverses that:
upload the bundle, enter the password, and (on an empty instance) it applies
cleanly, or (on a non-empty instance) a guarded force-replace path snapshots,
wipes, and re-applies with automatic rollback on failure.
There is no password recovery. The backup password derives the
encryption key directly — MagStacker never stores it, and there is no
"forgot password" path for a backup file. If you lose the password to a
backup, that backup is permanently unreadable; the export screen warns you
of this before you commit. Keep the password somewhere durable (a password
manager, not a sticky note on the server) and separate from the bundle
itself — storing them together defeats the point of encrypting the bundle.
See also#
docs/deployment.md— first-run setup, secrets,
TLS/reverse-proxy configuration, and the volumes defined in
docker-compose.yml.README.md— quick-start and the plainpg_dump
backup command for local tooling.