Self-Hosted Secrets With Tailscale and Infisical - A Homelab With No Plaintext Secrets
Self-host Infisical as the single source of truth for every homelab stack, reach it from anywhere over Tailscale, and lock it down with tags and grants without exposing anything to the public internet.
Looking to improve your secret management processes?Talk to an expert
Self-Hosted Secrets for Your Homelab With Tailscale and Infisical
The problem: a homelab full of .env files
Every stack in a homelab tends to have a `.env` file sitting next to it. The same database password gets copied into three compose folders, a backup, a deploy script, and a note somewhere. On a private network none of that is catastrophic, but there is no single source of truth, and every copy is one more place a secret can leak from.
This walkthrough fixes that with two free tools. Infisical, self-hosted on a box at home, holds exactly one copy of every secret, and each stack fetches its passwords from it at startup. Tailscale makes that secrets manager reachable from anywhere, including a phone on cellular, without opening a single port to the public internet. Change a secret once, restart the stacks that use it, and they pick up the new value.
If you want a broader look at running Infisical at home, our self-hosting Infisical video covers the basics. This one focuses on combining Infisical with Tailscale.
How the two layers fit together
Tailscale builds a private mesh network out of your own devices, called a tailnet. Every device connects to every other device over an encrypted tunnel, with no inbound ports to open. Your phone, laptop, Mac mini, and Raspberry Pi can all reach each other from whatever network they happen to be on.
The two products split the job cleanly:
- Tailscale decides who can reach Infisical. It makes the instance available anywhere and controls which devices are allowed to connect.
- Infisical decides what each caller can read. Every stack gets its own machine identity, scoped by project and role, so one app cannot read another app's secrets.
The pairing is deliberate. Tailscale's own guidance is to keep its keys in a secrets manager, and Infisical assumes you have a safe way to reach it.
Setting up the tailnet
Create a free Tailscale account. The personal plan covers up to six users and unlimited devices, and everything in this guide runs on free tiers.
You need at least one Linux box: a Raspberry Pi, an old laptop, or a VM. The demo uses three Mac minis running Linux VMs, `mini1` for Infisical and `mini2` and `mini3` for app stacks, but everything works the same on a single machine.
On each Linux box, run the install command from the Tailscale console, then bring the device onto the tailnet:
```bash sudo tailscale up ```
Open the printed URL, authenticate, and approve the device. Phones and laptops join through the Tailscale app. Once every device shows up in the admin console, they can reach each other by name from any network, with no router configuration or port forwarding.
Deploying Infisical with Docker Compose
On `mini1`, follow the Docker Compose self-hosting guide. With Docker and Docker Compose installed, download the compose file and the example `.env`, then lock down the `.env` file's permissions. The compose file runs Infisical, a Postgres database, and Redis.
Before starting anything, replace the defaults in `.env` (see the full list of environment variables):
- `ENCRYPTION_KEY` encrypts everything in the database. Generate your own with `openssl rand -hex 16`.
- `AUTH_SECRET` signs login sessions. Generate it with `openssl rand -base64 32`.
- Change the default Postgres username, password, and database name, even though the database is internal.
- Leave `SITE_URL` for now. You will set it once the machine has a stable HTTPS address.
As shipped, the compose file publishes Infisical on port 80, so any device on the local network could reach it by IP. Bind it to loopback instead so only processes on the machine itself, including Tailscale, can talk to it:
```yaml ports: - "127.0.0.1:8080:8080" ```
Then start the stack with `docker compose up -d`. Infisical is now running, and nothing but `mini1` itself can reach it.
Giving Infisical an HTTPS address with Tailscale Serve
Every device on a tailnet gets a DNS name automatically through MagicDNS, in the form `mini1vm.your-tailnet.ts.net`. Tailscale can also issue a real Let's Encrypt certificate for that name, so every browser, including the one on your phone, trusts it without a reverse proxy.
Turn on HTTPS certificates under DNS in the admin console. One thing to know first: the name of any machine that gets a certificate is published in a public Certificate Transparency log. That is how HTTPS works everywhere, not a Tailscale quirk. A name like `mini1vm` reveals nothing, while `secrets-server` would advertise what it runs forever. If you want a friendlier tailnet name, rename it before enabling certificates, because you cannot change it afterward.
Then point Tailscale Serve at Infisical:
```bash sudo tailscale serve --bg 8080 ```
Tailscale now answers HTTPS on port 443 and forwards traffic to Infisical on 8080. Copy the printed address into `SITE_URL` in `.env`, then run `docker compose up -d` again. A plain restart does not pick up the new `.env`.
Open that address from your laptop and create the first account, which becomes the super admin. Name the organization and set sign-ups to invite only, since this instance is just for your homelab. Infisical is now running with zero open ports, and every device on the tailnet can reach it by name.
Organizing secrets with projects, environments, and folders
A project is the boundary for one application or service. Here, a Nextcloud project holds Nextcloud's secrets and nothing else, and a Grafana project holds Grafana's.
Each project ships with development, staging, and production environments, and you can add your own. The same secret name can hold a different value in each environment, so there is no need to build naming schemes to separate dev from prod. Within an environment, folders add another layer of organization. Self-hosted Infisical puts no limit on projects, environments, or folders.
For a deeper tour of how secrets management works in Infisical, see our secrets management video.
Creating a machine identity for each stack
In the Nextcloud project's production environment, add the three secrets Nextcloud needs: `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `NEXTCLOUD_ADMIN_PASSWORD`. These are the values that would otherwise sit in a `.env` file next to the compose file.
Nextcloud cannot log in with your username and password. It needs its own identity. Under the organization's access control settings, create a machine identity called `nextcloud` with no organization-level access. Then set up Universal Auth, which gives the identity a client ID and client secret, effectively a username and password for a program. Infisical also supports platform-native methods such as AWS, Azure, GCP, and Kubernetes auth for workloads that have an identity of their own.
Back in the Nextcloud project, add the `nextcloud` identity with the viewer role. Viewers can read secret values but cannot change them, and this identity can read only the Nextcloud project. That scoping comes from Infisical's role-based access controls.
Injecting secrets into Docker Compose with the Infisical CLI
On `mini2`, install the Infisical CLI. The CLI logs in as the machine identity, fetches the project's secrets, and injects them into whatever process you run. Because the CLI defaults to Infisical Cloud, point it at your instance, then log in with the identity's credentials:
```bash export INFISICAL_DOMAIN="https://mini1vm.your-tailnet.ts.net" export INFISICAL_TOKEN=$(infisical login --method=universal-auth \ --client-id=<client-id> --client-secret=<client-secret> --silent --plain) ```
From here on, every CLI command on this box runs as the `nextcloud` identity. The Nextcloud compose file already references `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `NEXTCLOUD_ADMIN_PASSWORD`, so the old `.env` file with those values in plain text can be deleted.
Wrap the compose command in `infisical run`, passing the project ID from the project's settings page and the environment:
```bash infisical run --projectId=<project-id> --env=prod -- docker compose up -d ```
The CLI reports that it injected three secrets, and both containers start. The secrets are never written to disk. They exist only in the environment of the process, and Docker Compose fills them into the compose file as usual. For more on the CLI, see our Infisical CLI video.
A second stack: Grafana with no .env file at all
On `mini3`, set up Grafana with a Postgres database from scratch. The Grafana compose file and the Postgres data source provisioning file both reference `POSTGRES_PASSWORD` and `GF_SECURITY_ADMIN_PASSWORD`, and this machine never gets a `.env` file.
The steps repeat. Generate two random passwords with `openssl`, store them in the Grafana project's production environment, create a `grafana` machine identity with Universal Auth and no organization access, and add it to the Grafana project as a viewer. Install the CLI, export the domain and token, and run:
```bash infisical run --projectId=<grafana-project-id> --env=prod -- docker compose up -d ```
The CLI injects two secrets, and Grafana and Postgres both start. Testing the Postgres data source in Grafana confirms the connection works with a password that came straight from Infisical.
This is where centralizing pays off. To change the Postgres password, update it in Postgres, update it once in Infisical, and rerun the same command. Any number of stacks sharing that database pick up the new value with no hunting through `.env` files. For credentials that should change on a schedule, Infisical also supports automated secret rotation.
Locking down access with Tailscale tags and grants
By default, every device on a tailnet can reach every other device. That is fine for most things, but not for the box holding your secrets.
In the Tailscale admin console, open the access control policy in the JSON editor. Declare two tags, `tag:infisical` and `tag:homelab`. Tags are labels for machines that are not people. Then replace the default allow-all rule with a grant that lets devices tagged `tag:homelab` reach `tag:infisical` on port 443 only.
Save the policy and Infisical immediately becomes unreachable, because nothing is tagged yet. Tailscale only tells a device about machines it is allowed to reach, so from the laptop's point of view `mini1` no longer exists. Tag `mini1` as `tag:infisical` and `mini2` and `mini3` as `tag:homelab`, and `infisical run` works again on both app boxes.
To get your own devices back in, add a second grant from `autogroup:admin` to `tag:infisical`. Your laptop and phone can reach Infisical again, and anyone else you add to the tailnet cannot unless you grant it.
This adds a real layer of defense. If someone stole a machine identity's client ID and secret but was not on your tailnet, the credential would be useless: there is no network path to Infisical. The network decides who can reach Infisical, and Infisical's authentication and permissions decide what they can read.
Extending the model to CI with ephemeral nodes
The same model works for anything outside the homelab that needs secrets, as long as it can join the tailnet and authenticate to Infisical. A GitHub Actions job is a good example.
The first step of the job installs Tailscale on the runner and joins it to the tailnet as an ephemeral node, which exists only for the duration of the run. It joins with a tag, so the same grant that covers `mini2` and `mini3` covers the runner too.
Tailscale needs a credential to join, and that credential lives in GitHub as a secret. It does not have to be pasted in by hand. Infisical secret syncs push it from Infisical into GitHub, so Infisical stays the source of truth even for the key that grants network access. This works because Infisical only needs to be reachable inbound from tagged devices, while it can push secrets outbound to GitHub, AWS, Vercel, and more.
The next step uses the official Infisical GitHub Action. Normally the Infisical address has to be publicly reachable from GitHub's servers, but this runner is on the tailnet, so you give it the `ts.net` address and it pulls secrets from `mini1` over the tunnel. When the job finishes, the node disappears. Infisical never touches the public internet, and no long-lived credential in GitHub can fetch your secrets. For more on pairing Infisical with GitHub pipelines, see our GitHub CI/CD video.
Wrapping up
The end state: Infisical is reachable from a phone anywhere in the world, the compose stacks on `mini2` and `mini3` have no `.env` files next to them, and nothing is exposed to the public internet. No port forwarding, no public tunnels, no public login page.
Tailscale decides who can reach your secrets manager. Infisical decides what each person and machine can read. To try it yourself, start with the self-hosting overview.
Starting with Infisical is simple, fast, and free.