Skip to main content
Shipyard never stores your environment variables in plain text. Every secret you add to a project is encrypted before it reaches the database and is only decrypted at the moment a build container needs it. This page explains how encryption works and what you need to configure to keep your secrets safe.

What gets encrypted

Project secrets — the environment variables you add to a project — are encrypted and stored as ciphertext in the secrets table. The plaintext value is never persisted to the database.

Encryption scheme

Shipyard uses AES-256-GCM, an authenticated encryption algorithm that both encrypts data and detects any tampering. Each secret is encrypted as follows:
  1. A fresh random IV (initialization vector) is generated for every encryption operation using crypto.randomBytes(16).
  2. The plaintext is encrypted with your ENCRYPTION_KEY and the random IV.
  3. The GCM authentication tag is captured — this allows decryption to fail loudly if the ciphertext has been modified.
  4. The result is stored in the format <iv>:<authTag>:<ciphertext>, with each part encoded as a hex string.
The stored value in the database looks like:
a1b2c3d4e5f6...:deadbeef...:4f6e636521...
You cannot decrypt a secret without both the original ENCRYPTION_KEY and the exact IV that was used when it was encrypted.

When secrets are decrypted

Secrets are decrypted exactly once per build, immediately before the Docker container runs your build command. They are written to a .env file inside the isolated build temp directory and injected into the container’s environment. The temp directory is deleted automatically after the build completes.

What is never exposed

  • Secret values are never returned in API responses — list and detail endpoints omit plaintext secret data.
  • Projects that have secrets are never auto-deployed as static sites. Shipyard skips deployment for any project with associated secrets to prevent environment variables from being baked into publicly served output.

Generating the ENCRYPTION_KEY

Your ENCRYPTION_KEY must be a 32-byte value encoded as a 64-character hex string. Generate one with Node.js:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Add the output to your environment:
ENCRYPTION_KEY=your-64-character-hex-value-here

Webhook signature verification

GitHub signs every webhook payload it delivers using your WEBHOOK_SECRET. Shipyard verifies each incoming webhook with HMAC-SHA256 and Node.js’s timingSafeEqual, which prevents timing attacks that could allow an attacker to brute-force the secret by measuring how long comparison takes. Any request with an invalid or missing signature is rejected with 401. This ensures that only GitHub — using the secret you configured — can trigger a build.
If you rotate ENCRYPTION_KEY, all previously encrypted secrets become unreadable. Shipyard cannot decrypt them with a new key. Before rotating, delete all existing secrets from your projects and re-add them after updating the key.
In production, store ENCRYPTION_KEY and WEBHOOK_SECRET in a secrets manager or environment variable service (such as AWS Secrets Manager, Doppler, or your hosting provider’s secret store) rather than in a committed .env file.

Build docs developers (and LLMs) love