Skip to content

Secrets Management

Policy

Never Commit Secrets

All credentials must be stored in 1Password. Never commit secrets to the repository.

1Password CLI

Installation

brew install --cask 1password-cli
# Add 1Password apt repository
curl -sS https://downloads.1password.com/linux/keys/1password.asc | \
  sudo gpg --dearmor --output /usr/share/keyrings/1password-archive-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/1password-archive-keyring.gpg] https://downloads.1password.com/linux/debian/amd64 stable main" | \
  sudo tee /etc/apt/sources.list.d/1password.list
sudo apt update && sudo apt install 1password-cli

Basic Usage

# Retrieve a credential from scandora-automation vault
op item get "opnsense_api_key_owl_production" \
  --vault scandora-automation --fields api_key --reveal

# Use in scripts
API_KEY=$(op item get "zerotier_api_token_network_management" \
  --vault scandora-automation --fields api_token --reveal)

# List items by vault
op item list --vault scandora-automation
op item list --vault scandora-prd-automation

# List items by tag (across all vaults)
op item list --tags scandora.net

Authentication (Reducing Prompts)

For interactive development on luna (macOS):

  1. Ensure 1Password app is running and unlocked (uses Touch ID/biometric)
  2. 1Password CLI auto-connects to the running app
  3. No additional authentication needed for subsequent op commands
# Verify CLI is connected to 1Password app
op whoami

# Should show:
# URL:     https://my.1password.com
# Email:   joseph@scandora.net
# Account: scandoraproject

If not connected:

# Sign in once (will prompt for password and 2FA)
eval $(op signin)

# Subsequent commands use this session
# Session expires after 30 minutes of inactivity

Vault permissions:

The scandora-automation vault is configured for full read access to minimize prompts during development:

  • All automation scripts use this vault exclusively
  • Credentials are accessed without vault-specific authorization prompts
  • Production deployments work identically (no code changes needed)

Current Secrets

scandora-automation Vault

Infrastructure automation credentials (API keys, tokens):

Item Purpose Fields
opnsense_api_key_dev_vm Dev VM API access api_key, api_secret
opnsense_api_key_owl_production Owl gateway API api_key, api_secret
opnsense_api_key_blue_production Blue gateway API api_key, api_secret
zerotier_api_token_network_management ZeroTier Central API api_token
grafana_admin_password Grafana admin login password
snmp_community_monitoring SNMP access for Prometheus password
cloudflare_tunnel_token_pluto Cloudflare Zero Trust token
cloudflare_tunnel_token_dumbo Cloudflare Zero Trust token

scandora.net Vault

Cloud infrastructure and database credentials:

Item Purpose
aws_access_key_terraform_automation AWS CLI credentials (terraform, in scandora-automation)
gcp_service_account_key_scandoraproject_bigquery_admin BigQuery admin (scandoraproject)
gcp_service_account_key_coop_bigquery_admin BigQuery admin (coop-389306)
mysql_rds_scandora_mysql8_admin Legacy RDS credentials
cloudflare_api_token_dns_automation Public DNS management (in scandora-automation)
cloud_sql_scandora_postgres_user_joe PostgreSQL user (in scandora-automation)
gcp_postgres_grafana_password Grafana database (in scandora-automation)

1Password Service Account

Trusted cloud instances use the scandora-full service account for headless credential access.

Deployed Hosts

Host Token Location CLI Version Status
Pluto /etc/op-service-account.token 2.32.0 ✅ Active
Dumbo /etc/op-service-account.token 2.30.0 ✅ Active

Bogart is Untrusted

Bogart does not have 1Password access. No secrets should be stored there.

Usage on Trusted Hosts

# Export token and retrieve credential
export OP_SERVICE_ACCOUNT_TOKEN=$(sudo cat /etc/op-service-account.token)
op item get "aws_access_key_terraform_automation" --vault scandora-automation --fields access_key_id

# One-liner for scripts
API_KEY=$(OP_SERVICE_ACCOUNT_TOKEN=$(sudo cat /etc/op-service-account.token) \
  op item get "item_name" --vault scandora-automation --fields api_key)

Vault Required

Service accounts require --vault to be specified explicitly.

Token Management

  • Token stored in: 1Password Account (Joseph Scandora) → Security → scandora-full service account token
  • Vault access: scandora.net vault only (read-only)
  • To rotate: Generate new token in 1Password web console, redeploy to hosts

AWS Credentials (luna)

The luna workstation uses credential_process to retrieve AWS credentials from 1Password automatically.

Configuration

# ~/.aws/config
[default]
region = us-west-2
credential_process = /Users/joe/src/scandora.net/scripts/aws/op-credential-helper.sh

How It Works

  1. AWS CLI calls op-credential-helper.sh
  2. Script retrieves credentials from 1Password
  3. Returns JSON in AWS credential_process format
  4. No credentials stored on disk

See scripts/aws/README.md for details.

GCP Credentials (luna)

The luna workstation uses a session-based credential helper that retrieves GCP service account keys from 1Password.

Profiles

Multiple GCP projects are supported via profiles:

Profile Service Account Project Use Case
luna (default) scandora-owner scandoraproject dumbo, Cloud SQL
coop coop-owner coop-389306 bogart
coop-bq bigquery-admin coop-389306 BigQuery queries
scandora-bq scandora-bigquery-admin scandoraproject BigQuery queries

Configuration

Unlike AWS CLI, gcloud requires credentials via a file path (GOOGLE_APPLICATION_CREDENTIALS). The helper writes to a temp file that's automatically cleaned up on shell exit.

Usage

# Load default credentials (scandoraproject)
source scripts/gcp/gcp-env.sh

# Load coop credentials (for bogart)
source scripts/gcp/gcp-env.sh coop

# Aliases (add to ~/.bashrc or ~/.zshrc)
alias gcp-auth='source ~/src/scandora.net/scripts/gcp/gcp-env.sh'
alias gcp-coop='source ~/src/scandora.net/scripts/gcp/gcp-env.sh coop'

# Credentials now available
gcloud compute instances list
terraform plan

How It Works

  1. gcp-env.sh [PROFILE] calls op-credential-helper.sh --profile PROFILE
  2. Script retrieves service account JSON from 1Password via op document get
  3. Writes JSON to temp file with 0600 permissions
  4. Sets GOOGLE_APPLICATION_CREDENTIALS to temp file path
  5. Registers EXIT trap to clean up file when shell exits

See scripts/gcp/README.md for details.

Adding New Credentials

Best Practices

  1. Let 1Password generate credentials - Never generate externally
  2. Use descriptive names - Include service and host
  3. Tag with scandora.net - For easy filtering
  4. Document retrieval command - In repo files

Creating New Item

Choose the correct vault:

  • scandora-automation: Automation/API credentials (OPNsense, ZeroTier, monitoring, tunnels)
  • scandora.net: Cloud infrastructure keys and database passwords
# Create automation credential (use lowercase_with_underscores)
op item create --category="API Credential" \
  --title="service_name_description" \
  --vault="scandora-automation" \
  --tags="scandora.net" \
  --generate-password='letters,digits,symbols,32' \
  'api_key[password]=<generated>'

# Examples:
# - opnsense_api_key_dev_vm
# - zerotier_api_token_network_management
# - grafana_admin_password

Naming conventions:

  • Item names: lowercase_with_underscores (no spaces)
  • Field names: api_key, api_secret, api_token, password (consistent across all items)

Documenting in Repo

Reference 1Password items in code comments using the new naming convention:

# Credential: 1Password "opnsense_api_key_owl_production" (scandora-automation vault)
# Retrieve: op item get "opnsense_api_key_owl_production" --vault scandora-automation --fields api_key --reveal
opnsense_api_url: https://192.168.194.10
# Retrieve ZeroTier API token
ZEROTIER_TOKEN=$(op item get "zerotier_api_token_network_management" \
  --vault scandora-automation --fields api_token --reveal)

Credential Rotation

If a credential was ever in git history:

  1. Rotate immediately - Generate new credential in 1Password
  2. Update services - Deploy new credential to affected hosts
  3. Revoke old credential - If possible (API keys, tokens)
  4. Audit - Check for unauthorized access

Ansible Integration

Passing Secrets

Never store secrets in playbooks. Pass at runtime:

# Via extra-vars
TOKEN=$(op item get "Cloudflare Tunnel Token - pluto" --fields credential --reveal)
ansible-playbook playbooks/site.yml -e cloudflared_tunnel_token="$TOKEN"

# Via environment variable
export PDNS_API_KEY=$(op item get "PowerDNS API - Bogart" --fields credential --reveal)
ansible-playbook playbooks/dns.yml

Ansible Vault (Alternative)

For secrets that must be in files:

# Encrypt a file
ansible-vault encrypt secrets.yml

# Edit encrypted file
ansible-vault edit secrets.yml

# Run playbook with vault
ansible-playbook site.yml --ask-vault-pass