feat: Add SOPS state management and fix Terraform deployment issues
SOPS State Management: - Implemented Git + SOPS + age encryption for Terraform state files - Added .gitignore files to prevent committing unencrypted secrets - Created .sops.yaml.example template for age encryption configuration - Created helper scripts for automated encryption/decryption workflow: - scripts/tf: Wrapper script with auto-encrypt/decrypt - scripts/tf-encrypt: Manual encryption of state files - scripts/tf-decrypt: Manual decryption of state files - Added comprehensive STATE_MANAGEMENT.md documentation covering: - Installation of age and SOPS - Initial setup and key generation - Daily workflow examples - Security best practices - Troubleshooting common issues - Multi-user key management - Backup strategies Terraform Deployment Fixes: - Added snippets_storage variable for cloud-init snippet storage - Fixed datastore error: "local" does not support snippets - Updated README with solutions for datastore and SSH issues - Added troubleshooting for: - Enabling snippets on existing storage (pvesm set) - Creating dedicated directory storage for snippets - SSH authentication setup with ssh-agent - Manual cloud-init snippet creation workaround Files modified: - terraform/proxmox-examples/docker-host/main.tf - terraform/proxmox-examples/docker-host/variables.tf - terraform/proxmox-examples/docker-host/terraform.tfvars.example - terraform/proxmox-examples/docker-host/README.md Files added: - .gitignore (root level) - terraform/proxmox-examples/docker-host/.gitignore - terraform/proxmox-examples/docker-host/.sops.yaml.example - terraform/proxmox-examples/docker-host/STATE_MANAGEMENT.md - terraform/proxmox-examples/docker-host/scripts/tf - terraform/proxmox-examples/docker-host/scripts/tf-encrypt - terraform/proxmox-examples/docker-host/scripts/tf-decrypt
This commit is contained in:
parent
21d373a513
commit
9109712b59
11 changed files with 843 additions and 2 deletions
52
.gitignore
vendored
Normal file
52
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Terraform / OpenTofu
|
||||||
|
**/.terraform/
|
||||||
|
**/.terraform.lock.hcl
|
||||||
|
**/*.tfstate
|
||||||
|
**/*.tfstate.backup
|
||||||
|
**/*.tfstate.*.backup
|
||||||
|
**/crash.log
|
||||||
|
**/crash.*.log
|
||||||
|
**/.terraformrc
|
||||||
|
**/terraform.rc
|
||||||
|
**/override.tf
|
||||||
|
**/override.tf.json
|
||||||
|
**/*_override.tf
|
||||||
|
**/*_override.tf.json
|
||||||
|
**/.terraform.tfstate.lock.info
|
||||||
|
|
||||||
|
# Keep encrypted state files
|
||||||
|
!**/*.tfstate.enc
|
||||||
|
|
||||||
|
# SOPS
|
||||||
|
.sops/key.txt
|
||||||
|
**/.decrypted~*
|
||||||
|
|
||||||
|
# Environment files with secrets
|
||||||
|
**/.env
|
||||||
|
!**/.env.example
|
||||||
|
|
||||||
|
# Sensitive data directories
|
||||||
|
**/data/
|
||||||
|
**/config/
|
||||||
|
!**/config/*.example
|
||||||
|
!**/config/.gitkeep
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
**/logs/
|
||||||
|
**/*.log
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Editor files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.sublime-*
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
34
terraform/proxmox-examples/docker-host/.gitignore
vendored
Normal file
34
terraform/proxmox-examples/docker-host/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Terraform state files (unencrypted)
|
||||||
|
*.tfstate
|
||||||
|
*.tfstate.backup
|
||||||
|
*.tfstate.*.backup
|
||||||
|
|
||||||
|
# Keep encrypted state files
|
||||||
|
!*.tfstate.enc
|
||||||
|
|
||||||
|
# Terraform directory
|
||||||
|
.terraform/
|
||||||
|
.terraform.lock.hcl
|
||||||
|
|
||||||
|
# SOPS configuration with your private key
|
||||||
|
.sops.yaml
|
||||||
|
|
||||||
|
# Actual terraform.tfvars (may contain secrets)
|
||||||
|
terraform.tfvars
|
||||||
|
|
||||||
|
# Keep encrypted version
|
||||||
|
!terraform.tfvars.enc
|
||||||
|
|
||||||
|
# Crash logs
|
||||||
|
crash.log
|
||||||
|
crash.*.log
|
||||||
|
|
||||||
|
# Override files
|
||||||
|
override.tf
|
||||||
|
override.tf.json
|
||||||
|
*_override.tf
|
||||||
|
*_override.tf.json
|
||||||
|
|
||||||
|
# Terraform RC files
|
||||||
|
.terraformrc
|
||||||
|
terraform.rc
|
||||||
34
terraform/proxmox-examples/docker-host/.sops.yaml.example
Normal file
34
terraform/proxmox-examples/docker-host/.sops.yaml.example
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# SOPS Configuration for Terraform State Encryption
|
||||||
|
#
|
||||||
|
# Setup Instructions:
|
||||||
|
# 1. Install age and sops:
|
||||||
|
# - Debian/Ubuntu: sudo apt install age
|
||||||
|
# - macOS: brew install age sops
|
||||||
|
# - Manual: https://github.com/FiloSottile/age/releases
|
||||||
|
# https://github.com/getsops/sops/releases
|
||||||
|
#
|
||||||
|
# 2. Generate an age key:
|
||||||
|
# mkdir -p ~/.sops
|
||||||
|
# age-keygen -o ~/.sops/homelab-terraform.txt
|
||||||
|
#
|
||||||
|
# 3. Copy this file:
|
||||||
|
# cp .sops.yaml.example .sops.yaml
|
||||||
|
#
|
||||||
|
# 4. Replace YOUR_AGE_PUBLIC_KEY_HERE with the public key from step 2
|
||||||
|
# (the line starting with "age1...")
|
||||||
|
#
|
||||||
|
# 5. DO NOT commit .sops.yaml to git (it's in .gitignore)
|
||||||
|
# Keep your private key (~/.sops/homelab-terraform.txt) secure!
|
||||||
|
|
||||||
|
creation_rules:
|
||||||
|
# Encrypt all .tfstate files with age
|
||||||
|
- path_regex: \.tfstate$
|
||||||
|
age: YOUR_AGE_PUBLIC_KEY_HERE
|
||||||
|
|
||||||
|
# Encrypt any .secret files
|
||||||
|
- path_regex: \.secret$
|
||||||
|
age: YOUR_AGE_PUBLIC_KEY_HERE
|
||||||
|
|
||||||
|
# Encrypt terraform.tfvars (contains API tokens)
|
||||||
|
- path_regex: terraform\.tfvars$
|
||||||
|
age: YOUR_AGE_PUBLIC_KEY_HERE
|
||||||
|
|
@ -474,6 +474,80 @@ Type `yes` to confirm deletion.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Datastore Does Not Support Snippets
|
||||||
|
|
||||||
|
Error: `the datastore "local" does not support content type "snippets"`
|
||||||
|
|
||||||
|
**Cause:** The storage you specified doesn't have snippets enabled
|
||||||
|
|
||||||
|
**Solution 1 - Enable snippets on existing storage:**
|
||||||
|
```bash
|
||||||
|
# On Proxmox host, check current content types
|
||||||
|
pvesm status
|
||||||
|
|
||||||
|
# Enable snippets on local storage
|
||||||
|
pvesm set local --content backup,iso,vztmpl,snippets
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
pvesm status | grep local
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution 2 - Create dedicated directory storage:**
|
||||||
|
```bash
|
||||||
|
# On Proxmox host
|
||||||
|
# Create directory for snippets
|
||||||
|
mkdir -p /var/lib/vz/snippets
|
||||||
|
|
||||||
|
# Add directory storage via Proxmox UI:
|
||||||
|
# Datacenter → Storage → Add → Directory
|
||||||
|
# ID: local-snippets
|
||||||
|
# Directory: /var/lib/vz/snippets
|
||||||
|
# Content: Snippets
|
||||||
|
|
||||||
|
# Or via CLI:
|
||||||
|
pvesm add dir local-snippets --path /var/lib/vz/snippets --content snippets
|
||||||
|
|
||||||
|
# Update terraform.tfvars:
|
||||||
|
# snippets_storage = "local-snippets"
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSH Authentication Failed
|
||||||
|
|
||||||
|
Error: `failed to open SSH client: unable to authenticate`
|
||||||
|
|
||||||
|
**Cause:** The Proxmox provider needs SSH access to upload cloud-init files
|
||||||
|
|
||||||
|
**Solution 1 - Add SSH key to Proxmox (Recommended):**
|
||||||
|
```bash
|
||||||
|
# On your workstation, generate SSH key if you don't have one
|
||||||
|
ssh-keygen -t ed25519 -C "terraform@homelab"
|
||||||
|
|
||||||
|
# Copy to Proxmox host
|
||||||
|
ssh-copy-id root@proxmox.local
|
||||||
|
|
||||||
|
# Add key to ssh-agent
|
||||||
|
eval "$(ssh-agent -s)"
|
||||||
|
ssh-add ~/.ssh/id_ed25519
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
ssh-add -L
|
||||||
|
ssh root@proxmox.local "echo 'SSH works!'"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution 2 - Use API token only (workaround):**
|
||||||
|
|
||||||
|
If SSH is problematic, you can create the cloud-init snippet manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On Proxmox host, create the snippet
|
||||||
|
nano /var/lib/vz/snippets/cloud-init-docker-host.yaml
|
||||||
|
# Paste the cloud-init content from main.tf
|
||||||
|
|
||||||
|
# Then remove the proxmox_virtual_environment_file resource from main.tf
|
||||||
|
# and reference the file directly in the VM resource:
|
||||||
|
# user_data_file_id = "local:snippets/cloud-init-docker-host.yaml"
|
||||||
|
```
|
||||||
|
|
||||||
### Template Not Found
|
### Template Not Found
|
||||||
|
|
||||||
Error: `template with ID 9000 not found`
|
Error: `template with ID 9000 not found`
|
||||||
|
|
|
||||||
378
terraform/proxmox-examples/docker-host/STATE_MANAGEMENT.md
Normal file
378
terraform/proxmox-examples/docker-host/STATE_MANAGEMENT.md
Normal file
|
|
@ -0,0 +1,378 @@
|
||||||
|
# Terraform State Management with SOPS
|
||||||
|
|
||||||
|
This project uses [SOPS](https://github.com/getsops/sops) (Secrets OPerationS) with [age](https://github.com/FiloSottile/age) encryption to securely store Terraform state files in Git.
|
||||||
|
|
||||||
|
## Why SOPS + age?
|
||||||
|
|
||||||
|
✅ **Encrypted at rest** - State files contain sensitive data (IPs, tokens)
|
||||||
|
✅ **Version controlled** - Track infrastructure changes over time
|
||||||
|
✅ **No infrastructure required** - No need for S3, PostgreSQL, or other backends
|
||||||
|
✅ **Perfect for homelabs** - Simple, secure, self-contained
|
||||||
|
✅ **FOSS** - Fully open source tools
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
### 1. Install age
|
||||||
|
|
||||||
|
**Debian/Ubuntu:**
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install age
|
||||||
|
```
|
||||||
|
|
||||||
|
**macOS:**
|
||||||
|
```bash
|
||||||
|
brew install age
|
||||||
|
```
|
||||||
|
|
||||||
|
**Manual installation:**
|
||||||
|
```bash
|
||||||
|
# Download from https://github.com/FiloSottile/age/releases
|
||||||
|
wget https://github.com/FiloSottile/age/releases/download/v1.1.1/age-v1.1.1-linux-amd64.tar.gz
|
||||||
|
tar xzf age-v1.1.1-linux-amd64.tar.gz
|
||||||
|
sudo mv age/age age/age-keygen /usr/local/bin/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Install SOPS
|
||||||
|
|
||||||
|
**Debian/Ubuntu:**
|
||||||
|
```bash
|
||||||
|
# Download from https://github.com/getsops/sops/releases
|
||||||
|
wget https://github.com/getsops/sops/releases/download/v3.8.1/sops-v3.8.1.linux.amd64
|
||||||
|
sudo mv sops-v3.8.1.linux.amd64 /usr/local/bin/sops
|
||||||
|
sudo chmod +x /usr/local/bin/sops
|
||||||
|
```
|
||||||
|
|
||||||
|
**macOS:**
|
||||||
|
```bash
|
||||||
|
brew install sops
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify installation:
|
||||||
|
```bash
|
||||||
|
age --version
|
||||||
|
sops --version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Initial Setup
|
||||||
|
|
||||||
|
### 1. Generate Age Encryption Key
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create SOPS directory
|
||||||
|
mkdir -p ~/.sops
|
||||||
|
|
||||||
|
# Generate a new age key pair
|
||||||
|
age-keygen -o ~/.sops/homelab-terraform.txt
|
||||||
|
|
||||||
|
# View the key (you'll need the public key)
|
||||||
|
cat ~/.sops/homelab-terraform.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Output will look like:
|
||||||
|
```
|
||||||
|
# created: 2025-11-11T12:34:56Z
|
||||||
|
# public key: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
AGE-SECRET-KEY-1XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXx
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠️ IMPORTANT:**
|
||||||
|
- The line starting with `AGE-SECRET-KEY-1` is your **private key** - keep it secret!
|
||||||
|
- The line starting with `age1` is your **public key** - you'll use this in .sops.yaml
|
||||||
|
- **Backup this file** to a secure location (password manager, encrypted backup, etc.)
|
||||||
|
- If you lose this key, you **cannot decrypt** your state files!
|
||||||
|
|
||||||
|
### 2. Configure SOPS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd terraform/proxmox-examples/docker-host
|
||||||
|
|
||||||
|
# Copy the example config
|
||||||
|
cp .sops.yaml.example .sops.yaml
|
||||||
|
|
||||||
|
# Edit and replace YOUR_AGE_PUBLIC_KEY_HERE with your public key from step 1
|
||||||
|
nano .sops.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Your `.sops.yaml` should look like:
|
||||||
|
```yaml
|
||||||
|
creation_rules:
|
||||||
|
- path_regex: \.tfstate$
|
||||||
|
age: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
- path_regex: \.secret$
|
||||||
|
age: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
- path_regex: terraform\.tfvars$
|
||||||
|
age: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Set Environment Variable (Optional but Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add to your ~/.bashrc or ~/.zshrc
|
||||||
|
echo 'export SOPS_AGE_KEY_FILE=~/.sops/homelab-terraform.txt' >> ~/.bashrc
|
||||||
|
source ~/.bashrc
|
||||||
|
```
|
||||||
|
|
||||||
|
This tells SOPS where to find your private key for decryption.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Option A: Automatic Wrapper Script (Recommended)
|
||||||
|
|
||||||
|
Use the `./scripts/tf` wrapper that handles encryption/decryption automatically:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Initialize (first time)
|
||||||
|
./scripts/tf init
|
||||||
|
|
||||||
|
# Plan changes
|
||||||
|
./scripts/tf plan
|
||||||
|
|
||||||
|
# Apply changes (automatically encrypts after)
|
||||||
|
./scripts/tf apply
|
||||||
|
|
||||||
|
# Destroy infrastructure (automatically encrypts after)
|
||||||
|
./scripts/tf destroy
|
||||||
|
|
||||||
|
# View state
|
||||||
|
./scripts/tf show
|
||||||
|
```
|
||||||
|
|
||||||
|
The wrapper script:
|
||||||
|
1. Decrypts state files before running
|
||||||
|
2. Runs your terraform/tofu command
|
||||||
|
3. Encrypts state files after (if state was modified)
|
||||||
|
|
||||||
|
### Option B: Manual Encryption/Decryption
|
||||||
|
|
||||||
|
If you prefer manual control:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Decrypt state files
|
||||||
|
./scripts/tf-decrypt
|
||||||
|
|
||||||
|
# 2. Run terraform commands
|
||||||
|
tofu init
|
||||||
|
tofu plan
|
||||||
|
tofu apply
|
||||||
|
|
||||||
|
# 3. Encrypt state files
|
||||||
|
./scripts/tf-encrypt
|
||||||
|
|
||||||
|
# 4. Commit encrypted files to Git
|
||||||
|
git add *.enc
|
||||||
|
git commit -m "Update infrastructure"
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow Examples
|
||||||
|
|
||||||
|
### First Time Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd terraform/proxmox-examples/docker-host
|
||||||
|
|
||||||
|
# 1. Configure your variables
|
||||||
|
cp terraform.tfvars.example terraform.tfvars
|
||||||
|
nano terraform.tfvars # Add your API tokens, SSH keys, etc.
|
||||||
|
|
||||||
|
# 2. Initialize Terraform
|
||||||
|
./scripts/tf init
|
||||||
|
|
||||||
|
# 3. Plan infrastructure
|
||||||
|
./scripts/tf plan
|
||||||
|
|
||||||
|
# 4. Apply infrastructure
|
||||||
|
./scripts/tf apply
|
||||||
|
|
||||||
|
# 5. Encrypted state files are automatically created
|
||||||
|
# terraform.tfstate.enc now exists
|
||||||
|
|
||||||
|
# 6. Commit encrypted state to Git
|
||||||
|
git add terraform.tfstate.enc .sops.yaml.example
|
||||||
|
git commit -m "Add encrypted Terraform state"
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
### Making Infrastructure Changes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Decrypt, apply changes, re-encrypt (all automatic)
|
||||||
|
./scripts/tf apply
|
||||||
|
|
||||||
|
# 2. Commit updated encrypted state
|
||||||
|
git add terraform.tfstate.enc
|
||||||
|
git commit -m "Update VM configuration"
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cloning on a New Machine
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone the repository
|
||||||
|
git clone https://github.com/efigueroa/homelab.git
|
||||||
|
cd homelab/terraform/proxmox-examples/docker-host
|
||||||
|
|
||||||
|
# 2. Copy your age private key to the new machine
|
||||||
|
# (Securely transfer ~/.sops/homelab-terraform.txt)
|
||||||
|
mkdir -p ~/.sops
|
||||||
|
# Copy the key file here
|
||||||
|
|
||||||
|
# 3. Set up SOPS config
|
||||||
|
cp .sops.yaml.example .sops.yaml
|
||||||
|
# Edit with your public key
|
||||||
|
|
||||||
|
# 4. Decrypt state
|
||||||
|
./scripts/tf-decrypt
|
||||||
|
|
||||||
|
# 5. Now you can run terraform commands
|
||||||
|
./scripts/tf plan
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
### DO ✅
|
||||||
|
|
||||||
|
- **Backup your age private key** to multiple secure locations
|
||||||
|
- **Use different keys** for different projects/environments
|
||||||
|
- **Commit `.sops.yaml.example`** to Git (without your actual key)
|
||||||
|
- **Commit encrypted `*.enc` files** to Git
|
||||||
|
- **Use the wrapper script** to avoid forgetting to encrypt
|
||||||
|
|
||||||
|
### DON'T ❌
|
||||||
|
|
||||||
|
- **Never commit `.sops.yaml`** with your actual key (it's in .gitignore)
|
||||||
|
- **Never commit unencrypted `.tfstate`** files (they're in .gitignore)
|
||||||
|
- **Never commit unencrypted `terraform.tfvars`** with secrets
|
||||||
|
- **Never share your private age key** publicly
|
||||||
|
- **Don't lose your private key** - you can't decrypt without it!
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
terraform/proxmox-examples/docker-host/
|
||||||
|
├── .gitignore # Ignores unencrypted files
|
||||||
|
├── .sops.yaml # Your SOPS config (NOT in Git)
|
||||||
|
├── .sops.yaml.example # Template (in Git)
|
||||||
|
├── terraform.tfstate # Unencrypted state (NOT in Git)
|
||||||
|
├── terraform.tfstate.enc # Encrypted state (in Git) ✅
|
||||||
|
├── terraform.tfvars # Your config with secrets (NOT in Git)
|
||||||
|
├── terraform.tfvars.enc # Encrypted config (in Git) ✅
|
||||||
|
├── terraform.tfvars.example # Template without secrets (in Git)
|
||||||
|
├── scripts/
|
||||||
|
│ ├── tf # Wrapper script
|
||||||
|
│ ├── tf-encrypt # Manual encrypt
|
||||||
|
│ └── tf-decrypt # Manual decrypt
|
||||||
|
└── STATE_MANAGEMENT.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Error: "no key could decrypt the data"
|
||||||
|
|
||||||
|
**Cause:** SOPS can't find your private key
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```bash
|
||||||
|
# Set the key file location
|
||||||
|
export SOPS_AGE_KEY_FILE=~/.sops/homelab-terraform.txt
|
||||||
|
|
||||||
|
# Or add to ~/.bashrc permanently
|
||||||
|
echo 'export SOPS_AGE_KEY_FILE=~/.sops/homelab-terraform.txt' >> ~/.bashrc
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error: "YOUR_AGE_PUBLIC_KEY_HERE"
|
||||||
|
|
||||||
|
**Cause:** You didn't replace the placeholder in `.sops.yaml`
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```bash
|
||||||
|
# Edit .sops.yaml and replace with your actual public key
|
||||||
|
nano .sops.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error: "failed to get the data key"
|
||||||
|
|
||||||
|
**Cause:** The file was encrypted with a different key
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
- Ensure you're using the same age key that encrypted the file
|
||||||
|
- If you lost the original key, you'll need to re-create the state by running `tofu import`
|
||||||
|
|
||||||
|
### Accidentally Committed Unencrypted State
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```bash
|
||||||
|
# Remove from Git history (DANGEROUS - coordinate with team if not solo)
|
||||||
|
git filter-branch --force --index-filter \
|
||||||
|
'git rm --cached --ignore-unmatch terraform.tfstate' \
|
||||||
|
--prune-empty --tag-name-filter cat -- --all
|
||||||
|
|
||||||
|
# Force push (only if solo or coordinated)
|
||||||
|
git push origin --force --all
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lost Private Key
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
- Restore from your backup (you made a backup, right?)
|
||||||
|
- If truly lost, you'll need to:
|
||||||
|
1. Manually recreate infrastructure or import existing resources
|
||||||
|
2. Generate a new age key
|
||||||
|
3. Re-encrypt everything with the new key
|
||||||
|
|
||||||
|
## Advanced: Multiple Keys (Team Access)
|
||||||
|
|
||||||
|
If multiple people need access:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .sops.yaml
|
||||||
|
creation_rules:
|
||||||
|
- path_regex: \.tfstate$
|
||||||
|
age: >-
|
||||||
|
age1person1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
|
||||||
|
age1person2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
|
||||||
|
age1person3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
```
|
||||||
|
|
||||||
|
Each person's private key can decrypt the files.
|
||||||
|
|
||||||
|
## Backup Strategy
|
||||||
|
|
||||||
|
### Recommended Backup Locations:
|
||||||
|
|
||||||
|
1. **Password Manager** (1Password, Bitwarden, etc.)
|
||||||
|
```bash
|
||||||
|
# Copy the contents
|
||||||
|
cat ~/.sops/homelab-terraform.txt
|
||||||
|
# Store as a secure note in your password manager
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Encrypted USB Drive**
|
||||||
|
```bash
|
||||||
|
# Copy to encrypted drive
|
||||||
|
cp ~/.sops/homelab-terraform.txt /media/encrypted-usb/
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Encrypted Cloud Storage**
|
||||||
|
```bash
|
||||||
|
# Encrypt with gpg before uploading
|
||||||
|
gpg -c ~/.sops/homelab-terraform.txt
|
||||||
|
# Upload homelab-terraform.txt.gpg to cloud
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [SOPS Documentation](https://github.com/getsops/sops)
|
||||||
|
- [age Documentation](https://github.com/FiloSottile/age)
|
||||||
|
- [Terraform State Security](https://developer.hashicorp.com/terraform/language/state/sensitive-data)
|
||||||
|
- [OpenTofu Documentation](https://opentofu.org/docs/)
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
Common questions answered in this document:
|
||||||
|
- ✅ How do I set up SOPS? → See [Initial Setup](#initial-setup)
|
||||||
|
- ✅ How do I use it daily? → See [Option A: Automatic Wrapper](#option-a-automatic-wrapper-script-recommended)
|
||||||
|
- ✅ What if I lose my key? → See [Lost Private Key](#lost-private-key)
|
||||||
|
- ✅ How do I backup my key? → See [Backup Strategy](#backup-strategy)
|
||||||
|
- ✅ Can multiple people access? → See [Advanced: Multiple Keys](#advanced-multiple-keys-team-access)
|
||||||
|
|
@ -114,7 +114,7 @@ resource "proxmox_virtual_environment_vm" "docker_host" {
|
||||||
# Cloud-init user data for Docker installation
|
# Cloud-init user data for Docker installation
|
||||||
resource "proxmox_virtual_environment_file" "cloud_init_user_data" {
|
resource "proxmox_virtual_environment_file" "cloud_init_user_data" {
|
||||||
content_type = "snippets"
|
content_type = "snippets"
|
||||||
datastore_id = "local"
|
datastore_id = var.snippets_storage
|
||||||
node_name = var.proxmox_node
|
node_name = var.proxmox_node
|
||||||
|
|
||||||
source_raw {
|
source_raw {
|
||||||
|
|
|
||||||
76
terraform/proxmox-examples/docker-host/scripts/tf
Executable file
76
terraform/proxmox-examples/docker-host/scripts/tf
Executable file
|
|
@ -0,0 +1,76 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# tf - Wrapper for OpenTofu/Terraform with automatic SOPS encryption/decryption
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/tf init
|
||||||
|
# ./scripts/tf plan
|
||||||
|
# ./scripts/tf apply
|
||||||
|
# ./scripts/tf destroy
|
||||||
|
#
|
||||||
|
# This script automatically:
|
||||||
|
# 1. Decrypts state before running tofu commands
|
||||||
|
# 2. Runs your tofu command
|
||||||
|
# 3. Encrypts state after running tofu commands
|
||||||
|
#
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
TF_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
cd "$TF_DIR"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}ERROR: $1${NC}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
success() {
|
||||||
|
echo -e "${GREEN}✓ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
info() {
|
||||||
|
echo -e "${BLUE}ℹ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if tofu or terraform is installed
|
||||||
|
if command -v tofu &> /dev/null; then
|
||||||
|
TF_CMD="tofu"
|
||||||
|
elif command -v terraform &> /dev/null; then
|
||||||
|
TF_CMD="terraform"
|
||||||
|
else
|
||||||
|
error "Neither tofu nor terraform is installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Decrypt state if encrypted files exist
|
||||||
|
if [[ -f terraform.tfstate.enc || -f terraform.tfvars.enc ]]; then
|
||||||
|
info "Decrypting state files..."
|
||||||
|
"$SCRIPT_DIR/tf-decrypt"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run the terraform/tofu command
|
||||||
|
echo -e "${BLUE}Running: $TF_CMD $*${NC}"
|
||||||
|
echo
|
||||||
|
$TF_CMD "$@"
|
||||||
|
TF_EXIT_CODE=$?
|
||||||
|
|
||||||
|
# If the command succeeded and modified state, encrypt it
|
||||||
|
if [[ $TF_EXIT_CODE -eq 0 ]]; then
|
||||||
|
# Commands that modify state
|
||||||
|
if [[ "$1" =~ ^(apply|destroy|import|refresh|state)$ ]]; then
|
||||||
|
echo
|
||||||
|
info "Encrypting state files..."
|
||||||
|
"$SCRIPT_DIR/tf-encrypt"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $TF_EXIT_CODE
|
||||||
87
terraform/proxmox-examples/docker-host/scripts/tf-decrypt
Executable file
87
terraform/proxmox-examples/docker-host/scripts/tf-decrypt
Executable file
|
|
@ -0,0 +1,87 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# tf-decrypt - Decrypt Terraform state and tfvars files with SOPS
|
||||||
|
#
|
||||||
|
# Usage: ./scripts/tf-decrypt
|
||||||
|
#
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
TF_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
cd "$TF_DIR"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}ERROR: $1${NC}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
success() {
|
||||||
|
echo -e "${GREEN}✓ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo -e "${YELLOW}⚠ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if SOPS is installed
|
||||||
|
if ! command -v sops &> /dev/null; then
|
||||||
|
error "sops is not installed. Install it from: https://github.com/getsops/sops/releases"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if .sops.yaml exists
|
||||||
|
if [[ ! -f .sops.yaml ]]; then
|
||||||
|
error ".sops.yaml not found. Copy .sops.yaml.example and configure your age key."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if SOPS_AGE_KEY_FILE is set or exists in default location
|
||||||
|
if [[ -z "${SOPS_AGE_KEY_FILE:-}" ]]; then
|
||||||
|
if [[ -f ~/.sops/homelab-terraform.txt ]]; then
|
||||||
|
export SOPS_AGE_KEY_FILE=~/.sops/homelab-terraform.txt
|
||||||
|
else
|
||||||
|
warn "SOPS_AGE_KEY_FILE not set. Trying default age identities..."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🔓 Decrypting Terraform files..."
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Decrypt terraform.tfstate.enc if it exists
|
||||||
|
if [[ -f terraform.tfstate.enc ]]; then
|
||||||
|
echo "Decrypting terraform.tfstate.enc..."
|
||||||
|
sops -d terraform.tfstate.enc > terraform.tfstate
|
||||||
|
success "terraform.tfstate.enc → terraform.tfstate"
|
||||||
|
else
|
||||||
|
warn "terraform.tfstate.enc not found (this is normal for first-time setup)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Decrypt terraform.tfvars.enc if it exists
|
||||||
|
if [[ -f terraform.tfvars.enc ]]; then
|
||||||
|
echo "Decrypting terraform.tfvars.enc..."
|
||||||
|
sops -d terraform.tfvars.enc > terraform.tfvars
|
||||||
|
success "terraform.tfvars.enc → terraform.tfvars"
|
||||||
|
else
|
||||||
|
warn "terraform.tfvars.enc not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Decrypt backup state files if they exist
|
||||||
|
for backup_enc in terraform.tfstate.backup.enc terraform.tfstate.*.backup.enc; do
|
||||||
|
if [[ -f "$backup_enc" ]]; then
|
||||||
|
backup="${backup_enc%.enc}"
|
||||||
|
echo "Decrypting $backup_enc..."
|
||||||
|
sops -d "$backup_enc" > "$backup"
|
||||||
|
success "$backup_enc → $backup"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
success "All Terraform files decrypted successfully!"
|
||||||
|
echo
|
||||||
|
warn "Remember to encrypt files after making changes: ./scripts/tf-encrypt"
|
||||||
94
terraform/proxmox-examples/docker-host/scripts/tf-encrypt
Executable file
94
terraform/proxmox-examples/docker-host/scripts/tf-encrypt
Executable file
|
|
@ -0,0 +1,94 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# tf-encrypt - Encrypt Terraform state and tfvars files with SOPS
|
||||||
|
#
|
||||||
|
# Usage: ./scripts/tf-encrypt
|
||||||
|
#
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
TF_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
cd "$TF_DIR"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}ERROR: $1${NC}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
success() {
|
||||||
|
echo -e "${GREEN}✓ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo -e "${YELLOW}⚠ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if SOPS is installed
|
||||||
|
if ! command -v sops &> /dev/null; then
|
||||||
|
error "sops is not installed. Install it from: https://github.com/getsops/sops/releases"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if .sops.yaml exists
|
||||||
|
if [[ ! -f .sops.yaml ]]; then
|
||||||
|
error ".sops.yaml not found. Copy .sops.yaml.example and configure your age key."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if age key placeholder is still present
|
||||||
|
if grep -q "YOUR_AGE_PUBLIC_KEY_HERE" .sops.yaml; then
|
||||||
|
error ".sops.yaml contains placeholder. Replace YOUR_AGE_PUBLIC_KEY_HERE with your actual age public key."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🔐 Encrypting Terraform files..."
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Encrypt terraform.tfstate if it exists
|
||||||
|
if [[ -f terraform.tfstate ]]; then
|
||||||
|
echo "Encrypting terraform.tfstate..."
|
||||||
|
sops -e terraform.tfstate > terraform.tfstate.enc
|
||||||
|
success "terraform.tfstate → terraform.tfstate.enc"
|
||||||
|
|
||||||
|
# Securely delete unencrypted state
|
||||||
|
shred -u terraform.tfstate 2>/dev/null || rm -f terraform.tfstate
|
||||||
|
success "Deleted unencrypted terraform.tfstate"
|
||||||
|
else
|
||||||
|
warn "terraform.tfstate not found (this is normal if you haven't run 'tofu apply' yet)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Encrypt terraform.tfvars if it exists
|
||||||
|
if [[ -f terraform.tfvars ]]; then
|
||||||
|
echo "Encrypting terraform.tfvars..."
|
||||||
|
sops -e terraform.tfvars > terraform.tfvars.enc
|
||||||
|
success "terraform.tfvars → terraform.tfvars.enc"
|
||||||
|
|
||||||
|
# Keep original tfvars (don't delete, just warn)
|
||||||
|
warn "Remember to not commit unencrypted terraform.tfvars to Git"
|
||||||
|
else
|
||||||
|
warn "terraform.tfvars not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Encrypt backup state files if they exist
|
||||||
|
for backup in terraform.tfstate.backup terraform.tfstate.*.backup; do
|
||||||
|
if [[ -f "$backup" ]]; then
|
||||||
|
echo "Encrypting $backup..."
|
||||||
|
sops -e "$backup" > "${backup}.enc"
|
||||||
|
success "$backup → ${backup}.enc"
|
||||||
|
shred -u "$backup" 2>/dev/null || rm -f "$backup"
|
||||||
|
success "Deleted unencrypted $backup"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
success "All Terraform files encrypted successfully!"
|
||||||
|
echo
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. git add *.enc"
|
||||||
|
echo " 2. git commit -m 'Update encrypted Terraform state'"
|
||||||
|
echo " 3. git push"
|
||||||
|
|
@ -8,6 +8,12 @@ pm_tls_insecure = true
|
||||||
proxmox_node = "pve"
|
proxmox_node = "pve"
|
||||||
template_vm_id = 9000
|
template_vm_id = 9000
|
||||||
storage = "local-lvm"
|
storage = "local-lvm"
|
||||||
|
|
||||||
|
# Storage for cloud-init snippets (must support 'snippets' content type)
|
||||||
|
# Common options: "local" (directory storage), or create a directory storage
|
||||||
|
# See README.md for setup instructions if you get datastore errors
|
||||||
|
snippets_storage = "local"
|
||||||
|
|
||||||
network_bridge = "vmbr0"
|
network_bridge = "vmbr0"
|
||||||
|
|
||||||
# VM Configuration
|
# VM Configuration
|
||||||
|
|
|
||||||
|
|
@ -59,11 +59,17 @@ variable "disk_size" {
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "storage" {
|
variable "storage" {
|
||||||
description = "Storage pool name"
|
description = "Storage pool name for VM disks"
|
||||||
type = string
|
type = string
|
||||||
default = "local-lvm"
|
default = "local-lvm"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "snippets_storage" {
|
||||||
|
description = "Storage pool name for cloud-init snippets (must support 'snippets' content type)"
|
||||||
|
type = string
|
||||||
|
default = "local"
|
||||||
|
}
|
||||||
|
|
||||||
variable "network_bridge" {
|
variable "network_bridge" {
|
||||||
description = "Network bridge"
|
description = "Network bridge"
|
||||||
type = string
|
type = string
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue