- Shell 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| bin | ||
| hosts | ||
| secrets | ||
| .gitignore | ||
| .sops.yaml | ||
| README.md | ||
home_dir
Dotfiles synced across hosts. Secrets are encrypted at rest with SOPS + age and decrypted only when syncing.
Layout
.sops.yaml # encryption rules (which age keys can decrypt)
bin/
sops # sops binary
sync-dotfiles # main sync script
sops-encrypt # helper that encrypts files in place
public/ # plaintext files — copied verbatim to $HOME on each host
secrets/ # sops-encrypted files — decrypted during sync
hosts/
targets # rsync destinations, one per line (gitignored)
targets.example # template
public/ and secrets/ mirror the target home directory. A file at public/.bashrc lands at ~/.bashrc. A file at secrets/.ssh/id_ed25519.enc is decrypted and lands at ~/.ssh/id_ed25519.
Encrypted files always end in .enc. .gitignore refuses anything under secrets/ without that suffix, so a stray cp ~/.token secrets/.token can't be committed by mistake (use git add -f if you really mean to override).
Prerequisites
sops(bundled atbin/sops)agefor generating keys (pacman -S age/brew install age)rsyncandsshon both source and target hosts- An age private key at
~/.config/sops/age/keys.txton the source host
One-time setup on a new source host
mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
chmod 600 ~/.config/sops/age/keys.txt
grep '^# public key:' ~/.config/sops/age/keys.txt
Add that public key to .sops.yaml under age: (comma-separated with any existing keys), then re-encrypt existing secrets so the new key can decrypt them:
for f in $(find secrets -type f); do bin/sops updatekeys -y "$f"; done
Set up the hosts file:
cp hosts/targets.example hosts/targets
$EDITOR hosts/targets
Adding files
A new plaintext dotfile
Drop it under public/ at the path it should appear in $HOME:
cp ~/.vimrc public/.vimrc
A new secret file
Put the plaintext under secrets/ at the path it should appear in $HOME, then encrypt it:
mkdir -p secrets/.ssh
cp ~/.ssh/id_ed25519 secrets/.ssh/id_ed25519
bin/sops-encrypt secrets/.ssh/id_ed25519
# Result: secrets/.ssh/id_ed25519.enc (plaintext is shredded)
sops-encrypt writes <file>.enc and removes the plaintext input. The format is picked from the original extension:
*.yaml/*.yml/*.json/*.env/*.ini→ field-level encryption (keys stay readable, values encrypted — good for review-friendly diffs)- everything else → binary mode (whole file encrypted)
Editing an existing secret
For structured files (yaml/json/env/ini), sops opens an editor with plaintext and re-encrypts on save. Because the file ends in .enc, pass --input-type so sops knows the format:
bin/sops --input-type yaml --output-type yaml secrets/secrets.yaml.enc
For binary files (SSH keys, .my.cnf, etc.), decrypt → edit → re-encrypt:
bin/sops -d --input-type binary --output-type binary secrets/.my.cnf.enc > /tmp/edit.tmp
$EDITOR /tmp/edit.tmp
rm secrets/.my.cnf.enc
mv /tmp/edit.tmp secrets/.my.cnf
bin/sops-encrypt secrets/.my.cnf
Removing a file from sync
Delete it from public/ or secrets/. To also remove it from target hosts on the next sync, pass -d (rsync --delete).
Syncing
Always dry-run first:
bin/sync-dotfiles -nv
Then real run:
bin/sync-dotfiles
Flags
| Flag | Effect |
|---|---|
-n |
Dry run (rsync --dry-run) |
-v |
Verbose |
-d |
--delete extraneous files on the target |
-f FILE |
Use an alternate hosts file |
Syncing to a single ad-hoc host
Arguments override hosts/targets:
bin/sync-dotfiles -v greg@laptop.local:
The trailing colon matters — rsync needs it to recognize the path as a remote.
Adding a new target host
Each target only needs ssh access from the source — the source decrypts locally and pushes plaintext over SSH, so targets do not need their own age key.
- Make sure you can
ssh user@hostwithout a password prompt (key auth). - Add the destination to
hosts/targets. - Run
bin/sync-dotfiles -nvto dry-run, then drop-nonce it looks right.
How secrets are handled during sync
sync-dotfilescreates a staging directory under$TMPDIR(mode0700).- Files in
public/are copied in verbatim. - Files in
secrets/are decrypted into the staging dir at the same relative path, with the.encsuffix stripped; each ischmod 600and.ssh/becomes0700. The sync aborts if any file insecrets/lacks.enc. rsync -a -e sshpushes the staging tree to each target's$HOME.- On exit (including errors / Ctrl-C), a trap
shreds the staged files and removes the directory.
Plaintext only ever exists on disk on the source host (briefly, in a tmp dir) and on the target hosts (persistently, where it belongs). Set TMPDIR=/dev/shm if you want the staging dir in RAM.
Threat model notes
- An attacker with read access to this repo (e.g. on GitHub) sees only ciphertext.
- An attacker with the source host's age private key can decrypt everything. Treat
~/.config/sops/age/keys.txtas a root-equivalent secret. - Target hosts hold plaintext secrets on disk. Use full-disk encryption on every target.