Contents

Infisical and the Self-Hosted Secrets Question

A secrets manager with a UI, and whether a homelab has any business running one

Contents

Every homelab arrives at the secrets problem by the same road. You start with passwords typed into compose files. You feel bad about it, so you move them into .env files and add those to .gitignore. Now your repository describes a system that cannot actually start, your secrets exist in exactly one place with no backup, and rotating a database password means finding every file that mentions it by grep and hope.

At that point you go looking, and the ecosystem presents two philosophies. One says: encrypt the secrets and commit them, so the repo stays the whole truth — that is SOPS with age, and it is a file-shaped answer to a file-shaped problem. The other says: run a service that holds secrets, hands them out to authenticated callers, logs who asked, and rotates them on a schedule. That is HashiCorp Vault, and Vault is a lot of machine for a cupboard.

Infisical is the second philosophy without the Vault-shaped learning curve. It is open source, self-hostable in a compose file, has a genuinely pleasant web UI, and does the things people actually want: environments, per-project secrets, machine identities, versioning, and dynamic database credentials. I ran it for several months to find out whether the service model earns its keep at this scale. The answer is interesting and it is a firm “sometimes”.

What a secrets service buys you

Advertisement

The value of a secrets manager over encrypted files comes down to four things, and it is worth being precise about them because three are less useful at home than they sound.

Central rotation. Change a secret in one place and every consumer gets the new value on next fetch. With SOPS you edit the file, commit, and redeploy — which, if you have a GitOps loop, is roughly the same amount of work with a better audit trail. Score: draw.

Audit logging. Infisical records who read which secret and when. This is a compliance feature. At home, “who” is me. Score: mostly theatre.

Access control. Per-environment, per-path, per-identity policies. Genuinely useful the moment more than one person or more than one automation touches the lab, and useless before then. Score: depends entirely on you.

Dynamic secrets. This is the real one. Infisical can generate a Postgres user on demand with a 1-hour lease and drop it afterwards. Your application never holds a long-lived database password because no long-lived database password exists. Nothing file-based can do this, and it is the only capability here that changes what is possible rather than what is convenient.

If dynamic secrets do not interest you, the honest advice is to stop reading and use SOPS. If they do, keep going, because the rest of this is about the price.

Running it

Infisical needs Postgres and Redis, which tells you something about its ambitions. The compose stack is straightforward:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
services:
  infisical:
    image: infisical/infisical:latest-postgres
    restart: unless-stopped
    depends_on:
      db: { condition: service_healthy }
      redis: { condition: service_started }
    environment:
      ENCRYPTION_KEY: ${INFISICAL_ENCRYPTION_KEY}
      AUTH_SECRET: ${INFISICAL_AUTH_SECRET}
      DB_CONNECTION_URI: postgres://infisical:${DB_PASS}@db:5432/infisical
      REDIS_URL: redis://redis:6379
      SITE_URL: https://secrets.mylab.local
      SMTP_HOST: smtp.example.com
      SMTP_FROM_ADDRESS: [email protected]
    networks: [ edge, internal ]

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: infisical
      POSTGRES_PASSWORD: ${DB_PASS}
      POSTGRES_DB: infisical
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U infisical"]
      interval: 10s
    networks: [ internal ]

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    networks: [ internal ]

volumes:
  pgdata:

networks:
  edge:
    external: true
  internal:

Generate the two keys with openssl rand -hex 16 for ENCRYPTION_KEY and openssl rand -base64 32 for AUTH_SECRET. Lose ENCRYPTION_KEY and every secret in the database is permanently unreadable — it is the root of the whole thing, and it deserves the same treatment as a CA root key: written down, offline, in two places.

Notice what just happened. To hold your secrets, you now run three containers, a Postgres instance whose password is itself a secret, and an encryption key that cannot live in the system it protects. Which brings us to the interesting part.

The bootstrap problem

Advertisement

Every secrets service has this problem and every vendor’s documentation walks past it whistling. How does a client authenticate to the secrets manager without a secret?

Infisical’s answer is machine identities with several auth methods. The naive one is Universal Auth: a client ID and client secret, which you put in an environment variable. You have now replaced twelve secrets with one secret, which is progress of a kind — the twelve rotate centrally and the one does not. It is a real improvement and it is also not what the marketing implies.

The better answers remove the bootstrap secret entirely by binding identity to something the platform can vouch for:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Universal Auth: honest about being a secret.
infisical login --method=universal-auth \
  --client-id="$INFISICAL_CLIENT_ID" \
  --client-secret="$INFISICAL_CLIENT_SECRET"

# Fetch and exec, so the secret never touches disk.
infisical run --projectId="$PROJECT" --env=prod -- \
  /usr/local/bin/myapp --config /etc/myapp.toml

# Or render a file for services that only read files.
infisical export --format=dotenv --env=prod > /run/myapp/.env

On Kubernetes, service-account token auth means a pod proves its identity with a token the kubelet issued, and there is no bootstrap secret at all. That is the arrangement that genuinely closes the loop, and it is a large part of why secrets services make more sense on a cluster than on a host. If your lab is compose files on three mini PCs, you are living with a client secret in an env file, and you should be clear-eyed that this is the foundation everything else rests on.

Rotate that client secret on a schedule. Give each service its own identity rather than sharing one across the lab, so a compromise is contained and revocation is targeted. Both of these are ten minutes of work and both are routinely skipped.

Dynamic secrets, which are the actual reason

Here is where the service model earns its existence. Configure a dynamic secret backed by Postgres, and Infisical holds an admin credential and mints short-lived users on request:

1
2
3
4
-- The statement template Infisical runs on lease creation.
CREATE ROLE "{{username}}" WITH LOGIN PASSWORD '{{password}}'
  VALID UNTIL '{{expiration}}';
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "{{username}}";
1
2
3
4
# The application asks for credentials at start; they live one hour.
eval "$(infisical secrets get --projectId="$PROJECT" \
        --env=prod --path=/db --format=export)"
# PG_USER=v-grafana-a8f3d2  PG_PASS=...

The consequences are worth sitting with. There is no database password to steal from a config file, because there is no database password. A leaked credential expires within the hour. pg_stat_activity now shows you which lease is running an expensive query, which is an audit trail you did not have before. And revoking access to a service is a policy change rather than an expedition through eleven config files.

That is a real security property, and no amount of file encryption produces it. If you have a Postgres that several services touch — and if you run Paperless, Immich, Gitea and Nextcloud, you probably do — this is the argument.

The catch is the one you have already spotted: Infisical must be up for anything to start. A CA outage takes a day to hurt you; a dynamic-secrets outage takes an hour. You have placed a hard dependency at the boot path of your entire lab, and that dependency needs Postgres and Redis to be up first. Order your startup deliberately, and think hard about whether the thing that restores your backups should depend on it.

Where it fits, and what it displaces

Infisical does not replace Vaultwarden. Those are different problems: one holds machine credentials for services, the other holds human credentials for browsers. People conflate them and end up with their Netflix password in an API secrets manager, or their database credentials in a browser extension. Keep the line clean.

It overlaps directly with SOPS, and it partially overlaps with the Kubernetes secrets story — Infisical ships an operator that syncs into native Secrets, which is the same shape as External Secrets Operator with a different backend.

My honest split after living with both: SOPS for configuration secrets that change rarely and belong with the code — API tokens, webhook URLs, the credential your renewal script needs. They version with the repo, they restore with the repo, they need nothing running. Infisical for dynamic database credentials and for anything a second person touches, where the UI stops being a nicety and becomes the reason the other person can participate at all.

Running both is fine. They are not competing for the same job once you look closely.

Backing up the thing that holds everything

Advertisement

A secrets manager has an unusually nasty restore story, because the data is worthless without the key and the key is deliberately kept elsewhere. Get both halves or you have got nothing.

1
2
3
4
5
6
7
8
#!/usr/bin/env bash
set -euo pipefail
STAMP=$(date +%F)

# Half one: a consistent dump, never a copy of the data directory.
docker exec infisical-db-1   pg_dump -U infisical -Fc infisical > "/var/backups/infisical-$STAMP.dump"

restic -r "$RESTIC_REPO" backup "/var/backups/infisical-$STAMP.dump"

Half two is ENCRYPTION_KEY, and it must live somewhere the backup system does not, or a single compromise gives an attacker the ciphertext and the key together. Mine is on paper and on an encrypted USB stick, in the same envelope as the CA root passphrase, and it has never been in a password manager — because the password manager’s own credentials are the sort of thing people put in a secrets manager, and that circle has to be broken somewhere.

Then rehearse. Restore the dump into a throwaway stack with the real key, log in, read a secret. Twenty minutes, once, and you learn whether the ordering of your compose file actually lets the thing come back from cold. Snapshots are not backups and a pg_dump you have never restored is a hope rather than a plan.

The disaster worth thinking about in advance: Infisical is down and services will not start because they cannot fetch credentials. If your recovery procedure needs a database password that only Infisical has, you have built a knot. Keep a sealed break-glass copy of the handful of secrets needed to restore the lab — the backup repository password, the Postgres superuser, the CA passphrase — outside the system entirely. On paper is fine. That envelope is the difference between a bad evening and a rebuild.

Do not expose it

The same rule as every credential store: Infisical belongs on your internal network or behind a mesh VPN, with no public hostname. It runs a login form, it holds every machine credential in your lab, and its attack surface is a Node application under active development.

If people other than you need it, put Authentik in front — Infisical speaks OIDC, so you get real SSO with your existing identity provider and hardware-key second factors, rather than another password to manage. Failing that, keep it on the LAN and let the machines that need it be the only things that can reach it.

The Postgres instance in that compose file deserves a moment too. It is on an internal network with no ports: mapping, which is deliberate: nothing outside the stack has any business reaching it. Every guide that publishes 5432:5432 for convenience is teaching a bad habit, and this is the stack where the habit costs the most.

Troubleshooting

Login works in the browser, CLI returns 401. The CLI stores tokens per host and SITE_URL must match exactly. Check infisical login --domain https://secrets.mylab.local/api — the /api suffix is required for self-hosted and is easy to miss.

“Failed to decrypt secret” after a restore. ENCRYPTION_KEY differs from the one in use when the secret was written. There is no recovery. This is why the key lives offline, and why a restore rehearsal has to include it.

SMTP failures block user invitations. Infisical requires working email for signup verification. Without it, you can create the first admin and then nobody else, and the error surfaces as a generic 500. Configure SMTP before you invite anyone.

Everything is slow, Redis is fine. Postgres, almost always. Infisical is chatty and the container’s default shared_buffers is 128MB. A few of the settings that matter fix it in ten minutes.

Dynamic secret leases pile up as orphaned Postgres roles. Revocation failed — usually the role still owns objects, and Postgres refuses to drop it. Add REASSIGN OWNED BY "{{username}}" TO postgres; DROP OWNED BY "{{username}}"; to your revocation statement, or discover the problem at a thousand roles.

Container upgrade fails on migration. Migrations run at start and are one-way. Pin the tag, snapshot Postgres first, read the release notes. latest-postgres is a fine tag for a demo and a poor one for the service holding your credentials.

The verdict

Infisical is good software. The UI is the best I have used in this category, the CLI is sensible, the Kubernetes operator works, and it is genuinely self-hostable rather than self-hostable-with-an-asterisk. If you want a secrets manager, this is a better first choice than Vault for anyone who is not already a Vault person.

Whether you want one is the real question. For a single-operator homelab running compose files, it adds three containers, a Postgres dependency, an offline key you must protect, a bootstrap secret it cannot eliminate, and a service that must boot before anything else — in exchange for a UI and an audit log of your own activity. SOPS gives you most of the practical benefit for zero running infrastructure.

The moment it flips is dynamic database credentials, or a second person. If you want your services to hold hour-long Postgres leases instead of permanent passwords, nothing file-based can do it and the operational cost becomes a fair price. If you are reaching for it because .env files feel unprofessional, encrypt the files and spend the evening on your backups instead.

Advertisement
Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.