A surprising number of engineering teams handle credentials by copy-pasting .env files in direct messages or committing test keys into private repositories. When teams decide to fix this, they usually look at two extremes. On one side is HashiCorp Vault: enterprise-grade and capable, but requiring dedicated operators to manage Consul storage clusters, Shamir unseal ceremonies, and token lease renewals. On the other side are hosted services like Doppler: clean developer experience, but closed-source and billed per seat while storing your production secrets on third-party infrastructure.
Infisical emerged to bridge this gap. With roughly 30,000 GitHub stars and an open-source core under the MIT license, it has become the default self-hosted secret manager for teams moving away from HashiCorp's BSL license and expensive SaaS plans.
The interesting question is what happens under the hood. How does a command like infisical run -- npm start hand secrets to your code without creating a .env file? How does the database index encrypted secrets without decrypting them first? And how does it generate temporary database credentials that self-destruct after an hour?
Here is how Infisical works internally.
Why .env files are dangerous in production
To understand what Infisical does, look at what happens when an application loads credentials from a .env file on disk:
- Filesystem persistence: A
.envfile written to disk leaves traces in unallocated disk sectors, container image layers, and local cache backups. If you rundocker buildwith a.envfile present, that layer persists in your image history. - Workspace traversal: Modern developer tooling, language servers, and local AI coding assistants scan the project workspace by default. A plaintext file in your working directory is readable by any process running under your user account.
- Lack of auditability: When someone copies a database string from a
.envfile, there is no log entry, no timestamp, and no record of which developer accessed which key. - Static credentials: Static keys stay valid until someone manually rotates them, which in practice rarely happens until an incident forces it.
Infisical replaces this file-based model with an encrypted server, an API, and runtime process memory injection.
What actually happens during infisical run
When you start an application using Infisical, you run:
infisical run --env=production -- node server.js Most developers assume the CLI creates a temporary .env file, starts Node, and deletes the file. That is not what happens. Creating a temporary file would reintroduce the disk exposure risk.
Instead, Infisical uses POSIX process creation mechanisms:
+-------------------------------------------------------------+ | 1. Infisical CLI authenticates via Machine Identity / Token | +-------------------------------------------------------------+ | v +-------------------------------------------------------------+ | 2. Fetches encrypted secrets over TLS and decrypts in RAM | +-------------------------------------------------------------+ | v +-------------------------------------------------------------+ | 3. Builds environment array: ["PORT=3000", "DB_PASS=..."] | +-------------------------------------------------------------+ | v +-------------------------------------------------------------+ | 4. Invokes execve(binary, argv, envp) | | - Spawns child process directly | | - Passes envp into child process virtual memory stack | | - Zero bytes written to disk | +-------------------------------------------------------------+ Here is the exact step-by-step sequence:
- Authentication: The CLI reads an authentication token from
INFISICAL_TOKENor your local session, contacting the Infisical API over TLS 1.3. - In-Memory Decryption: The server returns the encrypted secret payload. The CLI decrypts the secrets in its own volatile memory using the project data key.
- Environment Block Assembly: The CLI formats the decrypted secrets into a standard C-style environment pointer array (
char *envp[]), combining the host environment with the injected variables. - Kernel System Call: The CLI calls the
execvesystem call (or platform equivalent likeCreateProcesson Windows), passing the command (node), arguments (server.js), and the customenvpblock.
Because the kernel loads the environment variables directly into the child process's initial stack and virtual memory segment, the secrets never touch the storage layer. When the child process terminates, the operating system reclaims the memory pages.
The shell expansion trap
A common issue trips up developers when testing infisical run:
# This outputs an empty line: infisical run --env=dev -- echo $DATABASE_URL # But this correctly prints the URL: infisical run --env=dev -- node -e "console.log(process.env.DATABASE_URL)" Why does the first command fail?
In the first command, your current shell interprets $DATABASE_URL before it executes the infisical binary. Because $DATABASE_URL does not exist in your parent shell, bash expands it to an empty string. The CLI receives infisical run -- echo "" and prints a blank line.
In the second command, the string argument is protected by quotes. Node starts inside the child environment where process.env.DATABASE_URL has been populated by execve.
Multilayer envelope encryption: How secrets are stored
Infisical uses PostgreSQL as its primary storage backend. If an attacker gains read access to the database via SQL injection or a leaked backup, they should not be able to read your credentials.
To achieve this, Infisical implements a four-tier envelope encryption architecture:
[ Root Encryption Key (AES-256) ] (Stored in server environment or external HSM / CloudHSM) | v encrypts at rest [ Internal KMS Root Key (AES-256) ] (Stored in PostgreSQL database) | v encrypts at rest [ Project & Organization Data Keys (AES-256-GCM) ] (One unique key per project, stored in PostgreSQL) | v encrypts at rest [ Secret Values & Credentials ] (Encrypted records in secrets_v2 table) The encryption layers
- Root Encryption Key: A 256-bit AES key supplied by the infrastructure operator via server environment variables, or stored inside a Hardware Security Module (HSM) such as AWS CloudHSM or Thales Luna. This key never leaves server memory.
- Internal KMS Root Key: Automatically generated when Infisical initializes. It acts as an intermediate key so the Root Encryption Key is only ever used to decrypt this single record.
- Project Data Key: Every organization and project receives its own distinct 256-bit AES key generated with cryptographically secure random bytes. This provides cryptographic isolation between tenants. Project data keys can also be delegated to external key managers like AWS KMS or GCP KMS.
- Secret Records: All secret values are encrypted with the Project Data Key using AES-256-GCM with a unique 96-bit nonce and a 128-bit authentication tag.
The Galois Counter Mode (GCM) authentication tag ensures confidentiality and tamper detection. If any ciphertext byte in PostgreSQL is altered, decryption fails immediately with an integrity error.
Blind indexing: Searching encrypted secrets without decrypting the table
A major architectural challenge in encrypted databases is search.
Suppose you have 10,000 secrets in your database, all encrypted with AES-256-GCM. A developer requests the secret named STRIPE_WEBHOOK_SECRET. How does the database find that record?
Traditional naive solutions fail:
- Decrypting every row: Running
SELECT * FROM secretsand decrypting thousands of rows in application memory on every query destroys database performance. - Deterministic encryption: Encrypting with a static initialization vector (IV) allows direct SQL lookup, but reveals equality patterns to attackers (frequency analysis).
Infisical resolves this with blind indexing, implemented in its cryptographic engine:
+--------------------+ | KMS Data Key | +--------------------+ | v HKDF with SHA-256 | Context: "infisical-secret-value-blind-index-v1" v +--------------------+ | Blind Index Key | +--------------------+ | +--------------+ | | Secret Name | ------------> ( HMAC-SHA256 ) +--------------+ | v +--------------------+ | Blind Index Hash | (Stored in indexed column) +--------------------+ Here is the exact mechanism:
- Key Derivation (HKDF): The system derives a separate blind index key from the KMS data key using HMAC-based Key Derivation Function (HKDF) with SHA-256. It applies a distinct context string (
infisical-secret-value-blind-index-v1). This domain separation ensures the derived key can only compute blind hashes and cannot decrypt ciphertext. - Deterministic Hashing: When a secret is saved or queried, Infisical computes
HMAC-SHA256(secret_name, blind_index_key). - Database Query: The resulting hash is stored in an indexed column. When searching, PostgreSQL executes:
SELECT * FROM secrets WHERE secret_blind_index = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; The database engine performs an efficient B-tree index lookup, but neither the database server nor an attacker reading the disk can deduce the underlying secret name.
Dynamic secrets: Ephemeral credentials with automatic revocation
Static credentials create security liability because they stay active indefinitely. If an engineer leaves the team or an access token is exposed in CI logs, the credential remains functional until someone remembers to update it.
Infisical handles this through dynamic secrets. Instead of storing static values, Infisical connects to target infrastructure (PostgreSQL, MySQL, AWS IAM, RabbitMQ) and creates temporary users on demand with strict Time-To-Live (TTL) leases.
How PostgreSQL dynamic secrets work
When an application or developer requests a temporary database user:
infisical dynamic-secrets lease create postgres-db --ttl=1h Infisical executes the configured SQL template against your PostgreSQL cluster:
-- Executed by Infisical admin user: CREATE ROLE "infisical_lease_9a2f1c" WITH LOGIN PASSWORD 'temporary_random_password' VALID UNTIL '2026-11-13 12:00:00+00'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO "infisical_lease_9a2f1c"; Infisical returns the generated username and password, then registers a delayed job in its BullMQ and Redis queue.
Application requests lease (TTL: 1h) | v Infisical creates user in Postgres with VALID UNTIL | v BullMQ schedules revocation job in Redis (delay: 3600s) | [1 hour passes] | v Worker executes revocation: REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM "infisical_lease_9a2f1c"; DROP ROLE IF EXISTS "infisical_lease_9a2f1c"; Even if the application crashes or forgets to release the lease, PostgreSQL enforces the VALID UNTIL timestamp at the database level, and Infisical's background worker drops the role completely.
Kubernetes synchronization without application changes
In production Kubernetes clusters, changing application code to call secret management SDKs creates vendor lock-in. If you write your application around an Infisical, Vault, or Doppler SDK, switching providers later requires rewriting your startup routines.
Infisical uses a Kubernetes Operator pattern written in Go. The operator monitors custom resource definitions (InfisicalSecret) and manages native Kubernetes Secret resources.
apiVersion: secrets.infisical.com/v1alpha1 kind: InfisicalSecret metadata: name: backend-secrets namespace: production spec: hostAPI: https://secrets.internal.example.com/api resyncInterval: 60 authentication: serviceAccount: tokenPath: /var/run/secrets/kubernetes.io/serviceaccount/token managedSecretReference: secretName: backend-app-env secretNamespace: production The operator reconciliation cycle works as follows:
- Authentication: The operator uses the projected Kubernetes Service Account Token to authenticate against Infisical via Kubernetes Native Auth.
- Sync: It fetches secrets for the specified project environment and writes them into a standard Kubernetes
Secretnamedbackend-app-env. - Consumption: Your deployment consumes secrets using standard Kubernetes syntax:
envFrom: - secretRef: name: backend-app-env - Auto-Reload: When secrets change in Infisical, the operator detects the update during its resync interval, updates the Kubernetes
Secret, and patches the deployment with a timestamp annotation. This triggers a standard zero-downtime rolling restart of the pods.
Your application code continues to read process.env or os.environ without any Infisical client libraries installed.
Architecture comparison: Infisical vs HashiCorp Vault vs Doppler vs .env
| Feature | Plain .env Files | HashiCorp Vault | Doppler | Infisical |
|---|---|---|---|---|
| Open Source | Yes (plain text) | No (BSL License) | No (Proprietary SaaS) | Yes (MIT Core) |
| Hosting Model | Local disk | Self-hosted / HCP | Cloud SaaS only | Self-hosted or Cloud |
| Storage Backend | Plain text files | Consul / Raft / Vault DB | Doppler Cloud | PostgreSQL + Redis |
| Operator Complexity | None (manual) | High (unsealing, policies) | Zero (hosted) | Low (single Docker Compose) |
| Memory Injection | No (reads from disk) | CLI / Agent | CLI (doppler run) | CLI (infisical run) |
| Blind Indexing | No | No | Proprietary | Yes (HKDF + HMAC-SHA256) |
| Dynamic Secrets | No | Yes | Limited | Yes (SQL, IAM, Redis, RabbitMQ) |
| Pricing | Free | Heavy infrastructure cost | $18 / user / month | Free self-hosted / Cloud tiers |
Self-hosting Infisical in two minutes
Because Infisical relies on standard components (PostgreSQL and Redis), running a self-hosted instance requires only a lightweight docker-compose.yml:
version: "3.8" services: db: image: postgres:16-alpine environment: POSTGRES_USER: infisical POSTGRES_PASSWORD: postgrespassword POSTGRES_DB: infisical volumes: - pgdata:/var/lib/postgresql/data redis: image: redis:7-alpine volumes: - redisdata:/data backend: image: infisical/infisical:latest depends_on: - db - redis ports: - "8080:80" environment: NODE_ENV: production DB_CONNECTION_URI: postgresql://infisical:postgrespassword@db:5432/infisical REDIS_URL: redis://redis:6379 ENCRYPTION_KEY: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef AUTH_SECRET: supersecretjwtkeymustbe32byteslong SITE_URL: http://localhost:8080 volumes: pgdata: redisdata: Once running, install the CLI via your package manager and link your local directory:
# Example with npm / homebrew / binary: npm install -g @infisical/cli # Login and link project infisical login infisical init # Run any application with injected variables infisical run -- npm run start Boundaries and realistic trade-offs
While runtime injection eliminates .env files on disk, developers should recognize the limits of process memory isolation:
- Child process inheritance: Any subprocess spawned by your application inherits the parent process environment by default. If your Node app runs
exec("bash script.sh"), the child script receives the same secrets unless you explicitly sanitize the environment block. - Process inspection via
/proc: On Linux, a user with root access or the same UID can inspect the environment of running processes by reading/proc/<pid>/environ. Moving secrets to RAM protects against filesystem leaks and accidental repository commits, but not against an attacker who has root access to the host. - Application logging: If your error logging framework dumps
process.envon unhandled exceptions, the secrets will end up in your Datadog, Sentry, or CloudWatch logs. Runtime injection prevents storage leaks, but application-level data sanitization remains necessary.
Key takeaways
- Zero disk footprint:
infisical runuses the POSIXexecvesystem call to inject decrypted credentials directly into the child process virtual memory block, avoiding.envdisk writes. - Multilayer cryptographic hierarchy: Secret records are encrypted with AES-256-GCM using project keys, which are wrapped by an internal KMS key and an operator root key. A leaked PostgreSQL dump yields no plaintext data.
- Blind indexing: Searching encrypted records uses HKDF key derivation and HMAC-SHA256, allowing fast B-tree index lookups without decrypting rows or revealing key names.
- Self-destructing credentials: Dynamic secrets create ephemeral database roles with database-enforced
VALID UNTILtimestamps, backed by BullMQ queue workers for cleanup. - Native Kubernetes integration: The Go operator synchronizes secrets directly to native Kubernetes
Secretobjects, allowing pods to consume environment variables without custom SDK dependencies.
