This project is a secure Symfony-based REST API for cryptographic operations using HashiCorp Vault. It leverages the Transit Engine for encryption, decryption, signing, and verification, along with a custom salt for enhanced security.
I want to provide a reference solution for how Symfony can cooperate with HashiCorp Vault for secure cryptographic operations.
During my DevOps work setting up Vault in production environments, I gained hands-on experience with its architecture and capabilities.
I also realized that the Transit Secrets Engine is a powerful way to offload encryption, decryption, and digital signing to Vault without managing keys manually in the application.
As a PHP developer who enjoys working with Symfony, I wanted to provide an example of how to use Vault’s Transit Secrets Engine within a Symfony application.
To demonstrate this integration, I built a simple and clean example using Symfony’s service architecture.
This project showcases how to encrypt, decrypt, sign, and verify data using Vault’s APIs securely from a Symfony app.
In today's world, data encryption is essential not just for compliance, but also for reducing the blast radius in case of a breach.
While using Vault as an external cryptographic service introduces performance tradeoffs due to network I/O and request latency, it’s a secure and centralized solution—especially valuable for high-trust environments where key management and auditability are critical.
.
├── docker
│ ├── nginx
│ │ ├── default.conf
│ │ └── certs/
│ └── vault
│ └── entrypoint.sh
├── docker-compose.yml
├── symfony
│ ├── src/
│ ├── config/
│ └── ...
├── test_vault_endpoints.sh
└── README.md ← (You are here)
- Encrypt and decrypt sensitive data using Vault's Transit Engine
- Sign and verify data with
ed25519key pair - Use application-level salt stored in Vault for secure signing
- Vault initialized automatically with keys and secrets via Docker entrypoint
- Rate-limiting protection against brute-force attacks on signature verification
- HTTPS-secured API access via Nginx
All endpoints are prefixed with: https://localhost:2443/api/crypto/v1
| Method | Path | Description |
|---|---|---|
| POST | /encrypt | Encrypt plaintext |
| POST | /decrypt | Decrypt ciphertext |
| POST | /sign | Sign plaintext + salt |
| POST | /verify | Verify signed message with salt |
Please create self-signed certs for nginx:
mkdir -p docker/nginx/certs
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout docker/nginx/certs/server.key \
-out docker/nginx/certs/server.crt \
-subj "/C=DE/ST=NRW/L=Duesseldorf/O=Dev/CN=localhost"`
and make sure the entrypoint.sh and test_vault_endpoints.sh file has the right permission:
chmod +x ./docker/vault/entrypoint.sh
chmod +x test_vault_endpoints.sh
Simply run docker compose up -d, after the containers are up, run the test script:
./test_vault_endpoints.shYou will see following:
🔐 Encrypting...
Ciphertext: vault:v1:something
🔓 Decrypting...
Plaintext: Hello World!
✍️ Signing...
Signature: vault:v1:something else
✅ Verifying...
Valid: true
🎉 Test successful complete!
File: docker/vault/entrypoint.sh
This script runs Vault in dev mode and performs:
-
Starts Vault server on
0.0.0.0:8200 -
Logs in with root token
-
Enables the
transitandkvsecret engines -
Creates:
- Data encryption key (DEK)
- Signing key (ed25519)
- Application salt as KV secret
Example excerpt:
vault secrets enable -path=transit transit
vault write -f "transit/keys/${DEK_KEY}"
vault write "transit/keys/${SIGN_KEY}" type=ed25519
vault secrets enable -path=secret kv
vault kv put "$KV_PATH" app_salt="My$ecretS@lt123"services:
php:
environment:
VAULT_ADDR: "http://vault:8200"
VAULT_TOKEN: "root"
VAULT_DEK_KEY: "symfony-dek-key"
VAULT_SIGN_KEY: "symfony-sign-key"
VAULT_KV_PATH: "secret/data/symfony-crypto-salt"
vault:
image: hashicorp/vault:1.14.2
entrypoint: ["/vault/entrypoint.sh"]File: src/Service/VaultService.php
The service handles all Vault interactions.
encrypt()→ uses Transit Enginedecrypt()→ uses Transit Enginesign()→ appends salt from KV before signingverify()→ appends salt to input before verifying
$salted = $input . $this->salt;
'input' => base64_encode($salted)To prevent brute-force attacks on /verify, the rate_limiter component is configured:
config/packages/rate_limiter.yaml
rate_limiter:
verify_signature:
policy: 'fixed_window'
limit: 5
interval: '1 minute'In controller:
try {
$valid = $this->vault->verify($text, $signature);
return $this->json(['valid' => $valid]);
} catch (ClientExceptionInterface $e) {
return $this->json(['error' => 'Invalid signature or malformed request.'], 400);
}- Vault never stores data, only provides cryptographic operations.
- Salt is stored securely in Vault's KV engine.
- Signature verification uses salted input to prevent rainbow table attacks.
- Rate limiter defends against repeated brute-force signature tests.
- No plaintext data stored in DB; encryption must be handled before persistence.
This VaultService is just a minimal example and should be adapted depending on the production Vault configuration. For example, in real deployments:
You can pass the Vault token dynamically per request:
$vaultToken = $this->getTokenForUser($userId); // Token mapped to Vault policy for user
$vaultService = new VaultService($http, $url, $vaultToken, ...);
The VaultService can also be injected into:
Doctrine Event Listeners to automatically encrypt/decrypt sensitive fields
Security/EventSubscriber to help sign or validate JWT claims
To dive deeper into the technologies and concepts used in this project:
- Vault Transit Secrets Engine – Official Docs
- Vault KV Secrets Engine – Official Docs
- Symfony HTTP Client
- Symfony RateLimiter Component
- HashiCorp Vault Production Hardening Guide
- How Vault Handles Secrets Engines and Paths
Happy encrypting your data! 🔐