Journey info · Platform: Docker Compose / GitHub · Time: ~45 minutes
This journey puts a Docker Compose instance's configuration under GitOps, with the repository's secrets unlocked by GPG keys instead of a shared git-crypt key file. Each person who manages the wiki unlocks the repository with their own GPG key, and the server gets a key of its own, so it can unlock its checkout without anyone forwarding a GPG agent to it. No secret file is passed between people, and a person's access is added or withdrawn by changing the list of recipients. It is a worked example, not the only way; for the concepts, see Help:GitOps, especially its Key management section, and Help:Secrets.
The examples use the instance ID mywiki, the wiki hostname wiki.example.com, the host name prod in the GitOps repository, and the repository git@github.com:example-org/wiki.example.com.git. Substitute your own.
Prerequisites
- A host running a Docker Compose Canasta instance that is working and configured the way you want it (see Help:Installation). GitOps takes the current configuration as its starting point.
- A shell on the host as the user that runs
canasta, withsudofor installing packages. - A GitHub account that can create a private repository in the organization or account that will own it. Any git host works; the deploy key steps differ slightly.
- A workstation (Linux or macOS) for your personal GPG key, and a password manager for its backup.
- Each other person who will unlock the repository creates a GPG key the same way and sends you the public key.
Phase 1: Create your GPG key
Do this on your workstation. The private key never leaves it, apart from the backup.
1. Install GnuPG
On macOS:
brew install gnupg pinentry-mac
pinentry-mac shows the passphrase prompt as a macOS dialog and can keep the passphrase in the Keychain. Point GnuPG at it (run gpg --list-keys once first if ~/.gnupg does not exist yet):
echo "pinentry-program $(brew --prefix)/bin/pinentry-mac" >> ~/.gnupg/gpg-agent.conf
On Debian or Ubuntu, sudo apt install gnupg.
2. Generate the key
gpg --full-generate-key
Accept the default kind of key (ECC, sign and encrypt) and curve (Curve 25519). For the expiration, enter 2y; an expiry limits the damage of a lost key, and you can extend it before it runs out. Enter your name, your email address and a strong passphrase.
Check the expiry dates. Both the primary key and its encryption subkey ([E]) should show the date you intended:
gpg --list-keys --keyid-format long you@example.com
The 40-character string under pub is the key's fingerprint. If a date is wrong, change both:
gpg --quick-set-expire <fingerprint> 2y
gpg --quick-set-expire <fingerprint> 2y '*'
3. Back up the key
Export the private key, store the file in your password manager, and delete it:
gpg --armor --export-secret-keys <fingerprint> > ~/gpg-private.asc
rm ~/gpg-private.asc # after storing it
Also store the revocation certificate GnuPG created in ~/.gnupg/openpgp-revocs.d/<fingerprint>.rev. It cancels the key if the key is ever compromised. In a macOS file chooser, Cmd+Shift+. shows hidden folders such as ~/.gnupg.
Repeat the backup whenever you change the key's expiry.
4. Export the public key
gpg --armor --export <fingerprint> > you.asc
The public key is not secret. Copy it to the host:
scp you.asc wiki.example.com:
Phase 2: Prepare the host
1. Install GnuPG and git-crypt
sudo apt install gnupg
canasta install git-crypt
2. Create the server's key
The server's key lets the host unlock its own checkout, for example after a restore, without a person forwarding a GPG agent to it. It has no passphrase, because GitOps commands run unattended. This adds little exposure: the instance's .env on the same host already holds every secret in plain text.
gpg --batch --passphrase '' --quick-generate-key "wiki.example.com server (gitops) <admin@example.com>" ed25519 cert,sign never
SERVER_FPR=$(gpg --list-secret-keys --with-colons "wiki.example.com server" | awk -F: '/^fpr/{print $10; exit}')
gpg --batch --passphrase '' --quick-add-key "$SERVER_FPR" cv25519 encr never
echo "$SERVER_FPR"
Note the fingerprint for Phase 4.
3. Import and trust your public key
git-crypt add-gpg-user only accepts a key that GnuPG trusts, so import your key and mark it trusted:
gpg --import ~/you.asc
echo "<fingerprint>:6:" | gpg --import-ownertrust
rm ~/you.asc
Check that both keys are listed, each with [ultimate]:
gpg --list-keys --keyid-format long
Phase 3: Create the repository and deploy key
1. Create an empty private repository
Create example-org/wiki.example.com as a private repository with no README, license or .gitignore. GitOps refuses a repository that already has commits. With the GitHub CLI:
gh repo create example-org/wiki.example.com --private
2. Add a deploy key for the host
On the host, follow Set up a deploy key: generate ~/.ssh/gitops_deploy with no passphrase, add the public half to the repository as a deploy key with write access, and add the Host github.com block to ~/.ssh/config. Then confirm:
ssh -T git@github.com
You should see a greeting naming the repository.
Phase 4: Initialize GitOps
1. Remove stray files
GitOps commits the instance's config/ directory. Remove or move out any leftover copies, such as Settings.php.bak, so they are not committed:
find ~/mywiki -path ~/mywiki/images -prune -o \( -name '*.bak' -o -name '*.orig' -o -name '*~' \) -print
2. Run init with your key as the recipient
canasta gitops init -i mywiki -n prod \
--repo git@github.com:example-org/wiki.example.com.git \
--git-crypt-gpg-user <fingerprint> \
--git-user-name "Your Name" --git-user-email you@example.com
You should see:
GitOps initialized for 'mywiki'. git-crypt key encrypted to GPG recipient <fingerprint>
The --git-user-name and --git-user-email options set the global git identity of the host's user, so every GitOps commit made on the host carries that name. See Production considerations if several people will commit from the same host.
3. Add the server's key
cd ~/mywiki
git-crypt add-gpg-user "$SERVER_FPR"
canasta gitops push -i mywiki
git-crypt add-gpg-user commits an encrypted copy of the repository key for that recipient under .git-crypt/keys/, and canasta gitops push pushes the commit.
4. Add other people
For each person who will unlock the repository, copy their public key to the host, then:
gpg --import ~/their.asc
echo "<their-fingerprint>:6:" | gpg --import-ownertrust
cd ~/mywiki && git-crypt add-gpg-user <their-fingerprint>
canasta gitops push -i mywiki
Give them access to the repository on GitHub as well.
Phase 5: Validation
1. Status is clean
canasta gitops status -i mywiki
You should see No changes. and Up to date with remote.
2. Each recipient is listed
ls ~/mywiki/.git-crypt/keys/default/0/
There is one .gpg file per recipient, named by fingerprint.
3. Secrets are encrypted on the remote
The host's vars files hold the real secret values. On GitHub they must be ciphertext:
cd ~/mywiki && git fetch -q && git show origin/main:hosts/prod/vars.yaml | head -c 10 | od -c | head -1
The output begins with \0 G I T C R Y P T. In env.template, which is not encrypted, every secret appears only as a placeholder such as {{mw_secret_key}}:
grep -E 'PASSWORD|SECRET|KEY' ~/mywiki/env.template
4. Unlock a clone with your own key
On your workstation, with git-crypt installed (brew install git-crypt on macOS, sudo apt install git-crypt on Debian or Ubuntu):
git clone git@github.com:example-org/wiki.example.com.git
cd wiki.example.com
git-crypt unlock
head -3 hosts/prod/vars.yaml
GnuPG asks for your passphrase, and the vars file is readable afterward. A recipient who is not on the list gets an error from git-crypt unlock instead.
Cleanup / teardown
To stop managing the instance through GitOps, in this order:
- On the host, remove the files GitOps added to the instance directory:
.git,.git-crypt,.gitattributes,.gitignore,.gitops-host,.gitops-applied,env.template,wikis.yaml.templateandhosts/. The instance keeps running from its.envandconfig/. - Delete the repository on GitHub. This also removes its deploy key.
- On the host, delete
~/.ssh/gitops_deployand~/.ssh/gitops_deploy.pub, and remove theHost github.comblock from~/.ssh/config. - On the host, delete the server's key:
gpg --delete-secret-and-public-key "$SERVER_FPR".
Your personal key is not tied to the instance; keep it for other repositories, or revoke it with its revocation certificate if you no longer need it.
Production considerations
- Removing a person's access. git-crypt cannot take a recipient off the list. Anyone who could unlock the repository already has its key, and the old commits stay readable to them. To withdraw access, create a new repository and initialize it again without that person, and rotate the secrets they could read (see Help:Secrets).
- Commit identity. Commits made on the host use the global git identity set at init. When several people commit from the same host account, each can override it for a session with
GIT_AUTHOR_NAME,GIT_AUTHOR_EMAIL,GIT_COMMITTER_NAMEandGIT_COMMITTER_EMAIL, or the host's identity can be set to a neutral name such as the server's. - Key expiry. git-crypt cannot add a recipient whose key has expired, so a teammate's key must be current when you add them. Extend personal keys before they run out, and back them up again afterward.
- Repository access. Organization members with access to private repositories can read the repository's unencrypted files, such as
config/andenv.template. Only recipients can read the vars files. - Making changes. Change configuration with
canasta config setand othercanastacommands, thencanasta gitops push. Apply changes made elsewhere withcanasta gitops pull, notgit pull, which skips rendering.envfrom the templates.
Troubleshooting
git-crypt add-gpg-usersays the key is not trusted- Mark the key trusted with
echo "<fingerprint>:6:" | gpg --import-ownertrustand run the command again.canasta gitops initrunsgit-crypt add-gpg-usertoo, so the same applies to the key named in--git-crypt-gpg-user.
gitops initcannot find the recipient- The recipient's public key must be in the host user's GPG keyring before init runs. Import it with
gpg --import.
Permission denied (publickey)on the initial push- The deploy key is missing, lacks write access, or is not selected by
~/.ssh/config. Check withssh -T git@github.com.
- GitOps reports the checkout is locked and cannot unlock it
- The host's keyring has no private key for any recipient. Add the server's key as a recipient from an unlocked checkout, or forward the GPG agent of a recipient to the host, as described in Key management.