secstore is a small Linux secret store for Docker Compose hosts. It encrypts
secrets on disk with age and hydrates them into a protected, non-swappable
tmpfs. It ships as a single native executable; no Go or age installation is
needed to run it.
Use a Linux host with tmpfs noswap support (upstream kernel 6.4+) and root
mounting privileges.
Download the .deb or .rpm package for your architecture from
GitHub Releases, then install it:
sudo apt install ./secstore_*.deb # Debian / Ubuntu
# or
sudo dnf install ./secstore-*.rpm # Fedora / RHELInitialize the store, add a secret through stdin, and hydrate it:
sudo secstore init
printf '%s' "$DB_PASSWORD" | sudo secstore add --name app/db_password
sudo secstore hydrateThe encrypted blob is stored at /var/lib/secstore/secrets/app/db_password.age.
The runtime file is available at /run/secrets-store/app/db_password.
Remove a secret with:
sudo secstore remove --name app/db_passwordTo hydrate before Docker starts, enable the included systemd service:
sudo systemctl daemon-reload
sudo systemctl enable --now secstore-hydrate.servicePackages do not automatically initialize the store or enable services.
The service uses 0444 file permissions for unprivileged container users;
root-owned 0700 host directories still restrict access.
Reference the hydrated host file as a Compose secret:
services:
app:
image: your-app-image
secrets:
- db_password
secrets:
db_password:
file: /run/secrets-store/app/db_passwordConfigure your application to read /run/secrets/db_password. After changing
a secret, hydrate again and recreate affected containers to refresh their
file bind mounts.
secstore --help lists the commands and environment overrides:
SECSTORE_VAULT_DIR=/var/lib/secstore
SECSTORE_RUN_DIR=/run/secrets-store
SECSTORE_RUN_SIZE=16M
SECSTORE_FILE_MODE=0400Back up identity.txt along with the encrypted blobs and protect the backup.
Anyone possessing both can decrypt the secrets.
See doc/design.md for storage layout, command behavior, hydration guarantees, security boundaries, migration from the original Bash script, systemd ordering, and troubleshooting.
Requires Go 1.26 or newer. The only direct library dependency is
filippo.io/age; the rest uses the standard library.
cmd/secstore/ Executable entrypoint
internal/secstore/ Implementation and tests
packaging/ Native package definition and systemd unit
doc/design.md Design and operational details
Build and install from source:
CGO_ENABLED=0 go build -trimpath -o secstore ./cmd/secstore
sudo install -m 0755 secstore /usr/bin/secstore
sudo install -m 0644 packaging/systemd/secstore-hydrate.service /etc/systemd/system/secstore-hydrate.serviceRun the unit tests and vet:
go test -race ./...
go vet ./...Linux mount tests use a private mount namespace and only synthetic secrets:
go test -c -o secstore.test ./internal/secstore
sudo unshare --mount --propagation private \
env SECSTORE_MOUNT_TEST=1 ./secstore.test -test.run '^TestRuntimeIntegration$'Optionally verify interoperability with an installed legacy age CLI:
SECSTORE_TEST_AGE="$(command -v age)" go test -run '^TestAgeCLICompatibility$' ./internal/secstorenFPM is a build-time tool only:
go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.47.0
export SECSTORE_VERSION=0.0.0-dev
export SECSTORE_ARCH=amd64
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -o dist/secstore ./cmd/secstore
nfpm package --config packaging/nfpm.yaml --packager deb --target dist/
nfpm package --config packaging/nfpm.yaml --packager rpm --target dist/For ARM64, set both SECSTORE_ARCH and GOARCH to arm64. For ARMv7, use
SECSTORE_ARCH=arm7, GOARCH=arm, and GOARM=7. The package architecture must
match the binary you build. Set SECSTORE_VERSION to the intended version;
CI uses 0.0.0-dev.<run number> for development snapshots. Release packages
use the stable tag version without its leading v.
packaging/archive.sh creates the archive and checksum list from dist/.
Run it from the repository root after building both native packages, with
SECSTORE_VERSION, SECSTORE_ARCH, and RUNNER_TEMP (a temporary staging
directory) set.
.github/workflows/ci.yaml checks formatting, vet, race-enabled tests, and
real noswap mounts, then uploads static binaries and .deb/.rpm packages
for amd64, arm64, and ARMv7, alongside archives and checksums. It runs for
branch pushes and pull requests, and is reused by the release workflow.
Development snapshots remain available as CI artifacts.
The release workflow must first be merged into main and included in the
commit being tagged. Choose an unused stable vX.Y.Z version and a reviewed
commit already in main. For example, to tag the fetched tip of main:
git fetch origin
git tag -a v1.2.3 origin/main -m "secstore v1.2.3"
git push origin refs/tags/v1.2.3Both annotated and lightweight tags are supported. The
release workflow rejects prerelease tags,
build metadata, leading-zero version components, commits outside main, and
existing releases (including drafts). Tags without the v prefix do not
trigger releases.
Release builds use the exact tagged commit and the same checks and packaging as ordinary CI. Tests run on the Linux runner's native architecture; ARM packages are cross-compiled. Native ARM execution and package installation/lifecycle tests are not part of the release gate.
Each release includes:
secstore-vX.Y.Z-linux-<arch>.tar.gz, where<arch>isamd64,arm64, orarm7(ARMv7).secstore_X.Y.Z_<arch>.deb, using Debian architecturesamd64,arm64, orarmhf(ARMv7).secstore-X.Y.Z-1.<arch>.rpm, using RPM architecturesx86_64,aarch64, orarmv7hl(ARMv7).SHA256SUMScovering all downloadable archives and packages.
Archives have a versioned root containing bin/secstore, the systemd unit
under packaging/systemd/, the README, design document, and license. They do
not initialize a store or install/enable a service. Native packages remain the
recommended installation method.
Download SHA256SUMS alongside your chosen assets and verify them on Linux:
sha256sum --check --ignore-missing SHA256SUMSAfter all checks pass, the workflow downloads the same run's build artifacts,
verifies their exact filenames and checksums, and rechecks the tag's commit.
It creates a draft normal release, not a prerelease. Review the assets,
source commit, and workflow results in Releases, then publish manually.
The workflow never creates or moves tags, overwrites releases, or publishes
automatically. It uses the built-in GITHUB_TOKEN; no additional secret is
required.
Build, source, API, or asset-verification failures prevent draft creation. Inspect the failed job's logs before retrying. Re-run failed jobs while the original artifacts remain available; use Re-run all jobs if artifacts have expired or need rebuilding so the replacement payloads pass the checks.
An existing draft or published release stops the run. A partially failed upload can leave an incomplete draft: inspect it and remove it manually only after review before retrying the same unchanged tag. For source fixes, merge the fix and push a new version tag. Never move an existing release tag.