367 lines
10 KiB
Markdown
367 lines
10 KiB
Markdown
# Secrets Management in Attune Worker Service
|
|
|
|
## Overview
|
|
|
|
The Attune Worker Service includes a robust secrets management system that securely stores, retrieves, and injects secrets into action execution environments. Secrets are encrypted at rest in the database and decrypted on-demand during execution.
|
|
|
|
## Architecture
|
|
|
|
### Components
|
|
|
|
1. **SecretManager** (`crates/worker/src/secrets.rs`)
|
|
- Core component responsible for secret operations
|
|
- Handles fetching, decryption, and environment variable preparation
|
|
- Integrated into `ActionExecutor` for seamless secret injection
|
|
|
|
2. **Database Storage** (`attune.key` table)
|
|
- Stores secrets with ownership scoping (system, pack, action, sensor, identity)
|
|
- Supports both encrypted and plaintext values
|
|
- Tracks encryption key hash for validation
|
|
|
|
3. **Encryption System**
|
|
- Uses **AES-256-GCM** for authenticated encryption
|
|
- Derives encryption key from configured password using SHA-256
|
|
- Generates random nonces for each encryption operation
|
|
|
|
## Secret Ownership Hierarchy
|
|
|
|
Secrets are organized in a hierarchical ownership model with increasing specificity:
|
|
|
|
### 1. System-Level Secrets
|
|
- **Owner Type**: `system`
|
|
- **Scope**: Available to all actions across all packs
|
|
- **Use Case**: Global configuration (API endpoints, common credentials)
|
|
|
|
### 2. Pack-Level Secrets
|
|
- **Owner Type**: `pack`
|
|
- **Scope**: Available to all actions within a specific pack
|
|
- **Use Case**: Pack-specific credentials, service endpoints
|
|
|
|
### 3. Action-Level Secrets
|
|
- **Owner Type**: `action`
|
|
- **Scope**: Available only to a specific action
|
|
- **Use Case**: Action-specific credentials, sensitive parameters
|
|
|
|
### Override Behavior
|
|
|
|
When an action is executed, secrets are fetched in the following order:
|
|
1. System secrets
|
|
2. Pack secrets (override system secrets with same name)
|
|
3. Action secrets (override pack/system secrets with same name)
|
|
|
|
This allows for flexible secret management where more specific secrets override less specific ones.
|
|
|
|
## Encryption Format
|
|
|
|
### Encrypted Value Format
|
|
```
|
|
nonce:ciphertext
|
|
```
|
|
|
|
Both components are Base64-encoded:
|
|
- **Nonce**: 12-byte random value (96 bits) for AES-GCM
|
|
- **Ciphertext**: Encrypted payload with authentication tag
|
|
|
|
Example:
|
|
```
|
|
Xk3mP9qRsT6uVwYz:SGVsbG8gV29ybGQhIFRoaXMgaXMgYW4gZW5jcnlwdGVkIG1lc3NhZ2U=
|
|
```
|
|
|
|
### Encryption Key Derivation
|
|
|
|
The encryption key is derived from the configured password using SHA-256:
|
|
|
|
```
|
|
encryption_key = SHA256(password)
|
|
```
|
|
|
|
This produces a 32-byte (256-bit) key suitable for AES-256.
|
|
|
|
### Key Hash Validation
|
|
|
|
Each encrypted secret can optionally store the hash of the encryption key used to encrypt it:
|
|
|
|
```
|
|
key_hash = SHA256(encryption_key)
|
|
```
|
|
|
|
This allows validation that the correct key is being used for decryption.
|
|
|
|
## Configuration
|
|
|
|
### Security Configuration
|
|
|
|
Add to your `config.yaml`:
|
|
|
|
```yaml
|
|
security:
|
|
# Encryption key for secrets (REQUIRED for encrypted secrets)
|
|
encryption_key: "your-secret-encryption-password-here"
|
|
|
|
# Or use environment variable
|
|
# ATTUNE__SECURITY__ENCRYPTION_KEY=your-secret-encryption-password-here
|
|
```
|
|
|
|
⚠️ **Important Security Notes:**
|
|
- The encryption key should be a strong, random password (minimum 32 characters recommended)
|
|
- Store the encryption key securely (e.g., using a secrets manager, not in version control)
|
|
- If the encryption key is lost, encrypted secrets cannot be recovered
|
|
- Changing the encryption key requires re-encrypting all secrets
|
|
|
|
### Environment Variables
|
|
|
|
Override configuration via environment variables:
|
|
|
|
```bash
|
|
export ATTUNE__SECURITY__ENCRYPTION_KEY="your-encryption-key"
|
|
```
|
|
|
|
## Usage Examples
|
|
|
|
### Storing Secrets (via API)
|
|
|
|
#### System-Level Secret
|
|
```bash
|
|
curl -X POST http://localhost:8080/api/v1/keys \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"ref": "system.api_endpoint",
|
|
"owner_type": "system",
|
|
"name": "api_endpoint",
|
|
"value": "https://api.example.com",
|
|
"encrypted": false
|
|
}'
|
|
```
|
|
|
|
#### Pack-Level Secret (Encrypted)
|
|
```bash
|
|
curl -X POST http://localhost:8080/api/v1/keys \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"ref": "mypack.api_key",
|
|
"owner_type": "pack",
|
|
"owner_pack": 1,
|
|
"name": "api_key",
|
|
"value": "sk_live_abc123def456",
|
|
"encrypted": true
|
|
}'
|
|
```
|
|
|
|
#### Action-Level Secret
|
|
```bash
|
|
curl -X POST http://localhost:8080/api/v1/keys \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"ref": "mypack.myaction.oauth_token",
|
|
"owner_type": "action",
|
|
"owner_action": 42,
|
|
"name": "oauth_token",
|
|
"value": "ya29.a0AfH6SMBx...",
|
|
"encrypted": true
|
|
}'
|
|
```
|
|
|
|
### Accessing Secrets in Actions
|
|
|
|
Secrets are automatically injected as environment variables during execution. The secret name is converted to uppercase and prefixed with `SECRET_`.
|
|
|
|
#### Python Action Example
|
|
|
|
```python
|
|
#!/usr/bin/env python3
|
|
import os
|
|
|
|
# Access secrets via environment variables
|
|
api_key = os.environ.get('SECRET_API_KEY')
|
|
db_password = os.environ.get('SECRET_DB_PASSWORD')
|
|
oauth_token = os.environ.get('SECRET_OAUTH_TOKEN')
|
|
|
|
if not api_key:
|
|
print("Error: SECRET_API_KEY not found")
|
|
exit(1)
|
|
|
|
# Use the secrets
|
|
print(f"Connecting to API with key: {api_key[:8]}...")
|
|
```
|
|
|
|
#### Shell Action Example
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
|
|
# Access secrets
|
|
echo "API Key: ${SECRET_API_KEY:0:8}..."
|
|
echo "Database: ${SECRET_DB_HOST}"
|
|
|
|
# Use in commands
|
|
curl -H "Authorization: Bearer $SECRET_API_TOKEN" \
|
|
https://api.example.com/data
|
|
```
|
|
|
|
### Environment Variable Naming Rules
|
|
|
|
Secret names are transformed as follows:
|
|
- Prefix: `SECRET_`
|
|
- Convert to uppercase
|
|
- Replace hyphens with underscores
|
|
|
|
Examples:
|
|
- `api_key` → `SECRET_API_KEY`
|
|
- `db-password` → `SECRET_DB_PASSWORD`
|
|
- `oauth_token` → `SECRET_OAUTH_TOKEN`
|
|
|
|
## Security Best Practices
|
|
|
|
### 1. Encryption Key Management
|
|
- **Generate Strong Keys**: Use at least 32 random characters
|
|
- **Secure Storage**: Store in a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.)
|
|
- **Rotation**: Plan for key rotation (requires re-encrypting all secrets)
|
|
- **Backup**: Keep encrypted backup of the encryption key
|
|
|
|
### 2. Secret Storage
|
|
- **Always Encrypt Sensitive Data**: Use `encrypted: true` for passwords, tokens, API keys
|
|
- **Plaintext for Non-Sensitive**: Use `encrypted: false` for URLs, usernames, configuration
|
|
- **Least Privilege**: Use action-level secrets for the most sensitive data
|
|
|
|
### 3. Action Development
|
|
- **Never Log Secrets**: Avoid printing secret values in action output
|
|
- **Mask in Errors**: Don't include secrets in error messages
|
|
- **Clear After Use**: In long-running processes, clear secrets from memory when done
|
|
|
|
### 4. Access Control
|
|
- **RBAC**: Limit who can create/read secrets using Attune's RBAC system
|
|
- **Audit Logging**: Enable audit logging for secret access (future feature)
|
|
- **Regular Reviews**: Periodically review and rotate secrets
|
|
|
|
## Implementation Details
|
|
|
|
### Encryption Process
|
|
|
|
```rust
|
|
// 1. Derive encryption key from password
|
|
let key = SHA256(password);
|
|
|
|
// 2. Generate random nonce
|
|
let nonce = random_bytes(12);
|
|
|
|
// 3. Encrypt plaintext
|
|
let ciphertext = AES256GCM.encrypt(key, nonce, plaintext);
|
|
|
|
// 4. Format as "nonce:ciphertext" (base64-encoded)
|
|
let encrypted_value = format!("{}:{}",
|
|
base64(nonce),
|
|
base64(ciphertext)
|
|
);
|
|
```
|
|
|
|
### Decryption Process
|
|
|
|
```rust
|
|
// 1. Parse "nonce:ciphertext" format
|
|
let (nonce_b64, ciphertext_b64) = encrypted_value.split_once(':');
|
|
let nonce = base64_decode(nonce_b64);
|
|
let ciphertext = base64_decode(ciphertext_b64);
|
|
|
|
// 2. Validate encryption key hash (if present)
|
|
if key_hash != SHA256(encryption_key) {
|
|
return Error("Key mismatch");
|
|
}
|
|
|
|
// 3. Decrypt ciphertext
|
|
let plaintext = AES256GCM.decrypt(encryption_key, nonce, ciphertext);
|
|
```
|
|
|
|
### Secret Injection Flow
|
|
|
|
```
|
|
1. ActionExecutor prepares execution context
|
|
2. SecretManager fetches secrets for action
|
|
a. Query system-level secrets
|
|
b. Query pack-level secrets
|
|
c. Query action-level secrets
|
|
d. Merge with later overriding earlier
|
|
3. Decrypt encrypted secrets
|
|
4. Transform to environment variables
|
|
5. Inject into execution context
|
|
6. Action executes with secrets available
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### "No encryption key configured"
|
|
**Problem**: Worker service cannot decrypt secrets.
|
|
|
|
**Solution**: Set the encryption key in configuration:
|
|
```yaml
|
|
security:
|
|
encryption_key: "your-encryption-key-here"
|
|
```
|
|
|
|
### "Encryption key hash mismatch"
|
|
**Problem**: The encryption key used to decrypt doesn't match the key used to encrypt.
|
|
|
|
**Solution**:
|
|
- Verify you're using the correct encryption key
|
|
- Check if encryption key was recently changed
|
|
- May need to re-encrypt secrets with new key
|
|
|
|
### "Decryption failed"
|
|
**Problem**: Secret cannot be decrypted.
|
|
|
|
**Causes**:
|
|
- Wrong encryption key
|
|
- Corrupted encrypted value
|
|
- Invalid format
|
|
|
|
**Solution**:
|
|
- Verify encryption key is correct
|
|
- Check secret value format (should be "nonce:ciphertext")
|
|
- Try re-encrypting the secret
|
|
|
|
### Secrets Not Available in Action
|
|
**Problem**: Environment variables like `SECRET_API_KEY` are not set.
|
|
|
|
**Checklist**:
|
|
- Verify secret exists in database with correct owner type
|
|
- Check secret name matches expected format
|
|
- Ensure action's pack has access to the secret
|
|
- Check worker logs for "Failed to fetch secrets" warnings
|
|
|
|
## API Reference
|
|
|
|
### SecretManager Methods
|
|
|
|
#### `fetch_secrets_for_action(action: &Action) -> Result<HashMap<String, String>>`
|
|
Fetches all secrets relevant to an action (system + pack + action level).
|
|
|
|
#### `encrypt_value(plaintext: &str) -> Result<String>`
|
|
Encrypts a plaintext value using the configured encryption key.
|
|
|
|
#### `prepare_secret_env(secrets: &HashMap<String, String>) -> HashMap<String, String>`
|
|
Transforms secret names to environment variable format.
|
|
|
|
## Future Enhancements
|
|
|
|
### Planned Features
|
|
- [ ] Secret versioning and rollback
|
|
- [ ] Audit logging for secret access
|
|
- [ ] Integration with external secret managers (Vault, AWS Secrets Manager)
|
|
- [ ] Automatic secret rotation
|
|
- [ ] Secret expiration and TTL
|
|
- [ ] Multi-key encryption (key per pack/action)
|
|
- [ ] Secret templates and inheritance
|
|
|
|
### Under Consideration
|
|
- [ ] Dynamic secret generation
|
|
- [ ] Just-in-time secret provisioning
|
|
- [ ] Secret usage analytics
|
|
- [ ] Integration with certificate management
|
|
|
|
## References
|
|
|
|
- [AES-GCM Encryption](https://en.wikipedia.org/wiki/Galois/Counter_Mode)
|
|
- [NIST SP 800-38D](https://csrc.nist.gov/publications/detail/sp/800-38d/final) - Recommendation for Block Cipher Modes of Operation: Galois/Counter Mode (GCM)
|
|
- [Key Management Best Practices](https://www.owasp.org/index.php/Key_Management_Cheat_Sheet) |