Post

Using SOPS Encryption with Komodo

Notes This guide assumes you already have a GitOps system setup with Komodo, if you don’t please follow FoxxMD’s guide here

We will cover using the sops binary and a age key to safely and securely store your secrets in a public git repo encrypted with sops.

Lets first set our $PERIPHERY_ROOT_DIRECTORY variable to be the same as what is set in our Komodo compose file.

1
export PERIPHERY_ROOT_DIRECTORY=/path/used/in/komodo/compose

for me this is as follows since i use Unraid

1
export PERIPHERY_ROOT_DIRECTORY=/mnt/user/appdata/komodo

Run the following commands

1
2
3
curl -LO https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64
mv sops-v3.9.4.linux.amd64 $PERIPHERY_ROOT_DIRECTORY/sops
chmod +x $PERIPHERY_ROOT_DIRECTORY/sops

Now ensure you have age installed, use your standard package manager to install this, apt, apk, etc

Once age is installed run

1
age-keygen -o $PERIPHERY_ROOT_DIRECTORY/.sops.key

Now all you need to do is use the following script in your GitOps repo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash

# Check if required env var is set
if [ -z "$PERIPHERY_ROOT_DIRECTORY" ]; then
    echo "Error: PERIPHERY_ROOT_DIRECTORY is not set." >&2
    exit 1
fi

export SOPS_AGE_KEY_FILE="$PERIPHERY_ROOT_DIRECTORY/.sops.key"

sops="$PERIPHERY_ROOT_DIRECTORY/sops"

# Only proceed if secret.enc.env exists
if [ -f "secret.enc.env" ]; then
    # Ensure sops exists and is executable
    if [ -f "$sops" ] && [ ! -x "$sops" ]; then
        chmod +x "$sops"
    fi
    # Decrypt and write to .env
    "$sops" -d secret.enc.env > .env
fi

Save this as something useful, for me i used decrypt-secrets.sh

Finally depending on where you have your stacks and where you save the decrypt-secrets.sh add the following to your komodo toml for each stack you want to decrypt secrets on

1
2
3
pre_deploy.command = """
bash ../../decrypt-secrets.sh
"""

I’m using ../../ as my stacks are at docker/stacks/stackname but my decrypt-secrets.sh file is 2 directory above at docker/

Remove any enviroment variables you have set in the stack, and redeploy and it should work!

Make sure any secrets are called secret.env

Optionally use this githook to ensure no unecrypted secrets get imported pre-commit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#!/bin/sh

# Debug: Print environment and basic info
echo "=== Starting encryption script ==="

# Define where your age public key for sops is stored
AGE_KEY_FILE=".sops/age/public.key"
if [ ! -f "$AGE_KEY_FILE" ]; then
  echo "Age key file not found: $AGE_KEY_FILE"
  exit 1
fi
PUBLIC_KEY=$(cat "$AGE_KEY_FILE")

# Function to compute SHA-256 hash of file content
compute_hash() {
  sha256sum "$1" | awk '{print $1}'
}

# Function to encrypt file with sops
encrypt_file() {
  local file=$1
  local ext=${file##*.}
  local enc_file="${file%.$ext}.enc.$ext"
  local sops_args="--encrypt --age $PUBLIC_KEY"

  # Add YAML-specific regex if needed
  if [ "$ext" = "yaml" ] || [ "$ext" = "yml" ]; then
    sops_args="$sops_args --encrypted-regex '^(data|stringData)$'"
  fi

  echo "Encrypting $file to $enc_file"
  if eval "sops $sops_args '$file'" > "$enc_file"; then
    local hash=$(compute_hash "$file")
    echo "# Hash: $hash" >> "$enc_file"
    echo "Successfully encrypted: $enc_file"
    git add "$enc_file"
    return 0
  else
    echo "Failed to encrypt $file"
    return 1
  fi
}

# Get list of staged files that match our patterns
echo "=== Checking staged files ==="
MATCHING_FILES=$(git diff --cached --name-only | grep -E '(.*secret\.yaml$|.*secret\.env$|.*\.auto\.tfvars$)')
echo "Matched files:"
echo "$MATCHING_FILES" | while read line; do
  echo "- $line"
done

# Process matching files
echo "=== Processing files ==="
for file in $MATCHING_FILES; do
  echo "Processing file: $file"
  
  # Skip if file is already encrypted
  if [[ "$file" == *.enc.* ]]; then
    echo "Skipping already encrypted file: $file"
    continue
  fi

  # Verify file exists
  if [ ! -f "$file" ]; then
    echo "Warning: File $file is staged but doesn't exist in working directory"
    continue
  fi

  current_hash=$(compute_hash "$file")
  ext=${file##*.}
  
  # Determine encrypted filename
  if [[ "$file" == *.auto.tfvars ]]; then
    enc_file="${file%.auto.tfvars}.enc.auto.tfvars"
  else
    enc_file="${file%.$ext}.enc.$ext"
  fi

  if [ -f "$enc_file" ]; then
    previous_hash=$(tail -n 1 "$enc_file" | grep '# Hash:' | cut -d' ' -f3)
    if [ "$current_hash" != "$previous_hash" ]; then
      echo "File has changed, re-encrypting: $file"
      if ! encrypt_file "$file"; then
        echo "Failed to encrypt $file"
        continue
      fi
    else
      echo "No changes detected in $file. Skipping encryption."
    fi
  else
    echo "No existing encrypted file found, encrypting: $file"
    if ! encrypt_file "$file"; then
      echo "Failed to encrypt $file"
      continue
    fi
  fi

  # Unstage the plaintext file
  git reset HEAD -- "$file"
  echo "Unstaged plaintext file: $file"
done

echo "=== Script completed ==="
exit 0

Note you will have to use interpolation for these secrets to be exposed to the container https://github.com/FoxxMD/compose-env-interpolation-example

For more information check out my repo here https://github.com/fma965/f9-homelab

This post is licensed under CC BY 4.0 by the author.