This appendix contains advanced topics, additional patterns, and ideas for extending your "Workstation as Code" setup. These topics go beyond the workshop basics and are provided for self-study and exploration. Not all have been fully tested at the time of writing - please submit PR's if you find any issues.
- Advanced BlueBuild
- Advanced Gopass
- Advanced Chezmoi
- Integration Patterns
- Real-World Examples
- Additional Tools
- Common Recipes
- Performance Optimization
- Security Hardening
- Resources and References
You can maintain multiple image variants from one repository:
# recipes/workstation.yml - for your laptop
name: bluefin-workstation
base-image: ghcr.io/ublue-os/bluefin-main
modules:
- type: rpm-ostree
install:
- wireshark
- vlc
- gimp
# recipes/server.yml - for servers
name: bluefin-server
base-image: ghcr.io/ublue-os/bluefin-main
modules:
- type: rpm-ostree
install:
- podman
- nginx
remove:
- firefox # Don't need GUI appsUpdate .github/workflows/build.yml to build both:
strategy:
matrix:
recipe:
- workstation.yml
- server.yml
steps:
- uses: blue-build/github-action@v1
with:
recipe: recipes/${{ matrix.recipe }}Run custom scripts during image build:
# config/scripts/post-install.sh
#!/bin/bash
# Set default editor system-wide
echo "export EDITOR=vim" > /etc/profile.d/editor.sh
# Configure automatic updates
systemctl enable rpm-ostreed-automatic.timer
# Set up custom system configuration
echo "Custom setup complete"Reference in recipe.yml:
modules:
- type: script
scripts:
- post-install.shAdd third-party repositories:
modules:
- type: rpm-ostree
repos:
- https://download.docker.com/linux/fedora/docker-ce.repo
install:
- docker-ce
- docker-ce-cli
- containerd.ioManage Flatpak applications:
modules:
- type: default-flatpaks
notify: true
system:
install:
- org.mozilla.firefox
- com.visualstudio.code
- com.spotify.Client
remove:
# Remove apps you don't want from base image
- org.gnome.CalculatorLayer in system-wide configurations:
modules:
- type: files
files:
# Systemd service
- source: services/my-service.service
destination: /etc/systemd/system/my-service.service
# Kernel parameters
- source: sysctl.conf
destination: /etc/sysctl.d/99-custom.conf
# Firewall rules
- source: firewall/custom.xml
destination: /etc/firewalld/zones/custom.xmlCreate custom modules for reusability:
# modules/developer-tools.yml
name: developer-tools
type: rpm-ostree
install:
- vim
- git
- tmux
- htop
- jq
- ripgrep
- bat
- fd-find
# recipes/recipe.yml
modules:
- from-file: developer-tools.ymlOrganize secrets into multiple stores:
# Work store
$ gopass init --store work --crypto age
# Personal store
$ gopass init --store personal --crypto age
# Use stores
$ gopass show work/gitlab/token
$ gopass show personal/banking/password
# List all stores
$ gopass mountsShare secrets with team members:
# Initialize shared store with multiple recipients
$ gopass init --store team --crypto age
# Add team member's public key
$ gopass recipients add --store team age1team-member-public-key
# Add secrets to shared store
$ gopass insert team/shared-api-key
# Team members can now decrypt with their private keyAdvanced Git integration:
# Configure automatic sync
$ gopass config autosync true
# Automatic push after changes
$ gopass config autopush true
# Automatic pull before access
$ gopass config autopull true
# Configure multiple remotes
$ cd ~/.local/share/gopass/stores/root
$ git remote add backup git@backup-server:gopass.git
$ git push backup mainIf you prefer GPG over AGE:
# Generate GPG key
$ gpg --full-generate-key
# Initialize gopass with GPG
$ gopass init your-gpg-key-id
# Works similarly but uses GPG keyringAutomate actions with hooks:
# Pre-commit hook: run tests
$ cat > ~/.local/share/gopass/stores/root/.git/hooks/pre-commit << 'EOF'
#!/bin/bash
# Verify no plaintext secrets
if git diff --cached | grep -i "password.*=.*[^*]"; then
echo "Potential plaintext password in commit!"
exit 1
fi
EOF
$ chmod +x ~/.local/share/gopass/stores/root/.git/hooks/pre-commitUse Gopass from other applications:
# JSON API
$ gopass show -o json work/api-key
# Use in scripts
#!/bin/bash
API_KEY=$(gopass show -o work/api-key)
$ curl -H "Authorization: Bearer $API_KEY" https://api.example.comCustomize password generation:
# Configure default length
$ gopass config generate.autoclip false
$ gopass config generate.length 32
# Generate with specific requirements
$ gopass generate site.com/user 24 --symbols=false
$ gopass generate site.com/user 32 --strict # Must include all char typesEncrypt files in your dotfiles repository:
# Add encrypted file
$ chezmoi add --encrypt ~/.config/secret-config
# Chezmoi encrypts with AGE before committing
# You can safely commit encrypted files to public reposUse advanced template functions:
# In a template file
{{ if eq .chezmoi.os "linux" }}
# Linux-specific config
{{ end }}
{{ if eq .chezmoi.osRelease.id "fedora" }}
# Fedora-specific
{{ end }}
{{ if stat (joinPath .chezmoi.homeDir ".work") }}
# If ~/.work file exists, this is a work machine
{{ end }}
# Include file contents
{{ include "common-aliases.sh" }}
# Execute command and include output
{{ output "command" "arg1" "arg2" }}
# Decrypt with AGE
{{ decrypt "encrypted-data" }}
# Template with JSON
{{ (index (fromJson (include "config.json")) "key") }}# dot_zshrc.tmpl
# ZSH Configuration
# Machine-specific configuration
{{ if eq .chezmoi.hostname "work-laptop" }}
# Work machine
export WORK_ENV=true
export COMPANY_VPN_CONFIG=/etc/company/vpn.conf
# Work-specific aliases
alias vpn='sudo openvpn $COMPANY_VPN_CONFIG'
alias deploy='kubectl apply -f'
{{ else if eq .chezmoi.hostname "personal-desktop" }}
# Personal machine
export PERSONAL_ENV=true
# Gaming aliases
alias steam='flatpak run com.valvesoftware.Steam'
{{ end }}
# Common configuration across all machines
export EDITOR={{ .editor | default "vim" }}
# Pull secrets from Gopass
{{ if stat (joinPath .chezmoi.homeDir ".local/share/gopass") }}
export GITHUB_TOKEN={{ output "gopass" "show" "-o" "github/token" }}
{{ end }}
# Include common aliases
{{ include "aliases.sh" }}Run setup scripts only once:
# run_once_install-fonts.sh.tmpl
#!/bin/bash
# This runs once, tracked by chezmoi
{{ if eq .chezmoi.os "linux" }}
echo "Installing custom fonts..."
mkdir -p ~/.local/share/fonts
# Download and install fonts
curl -L https://github.com/ryanoasis/nerd-fonts/releases/download/v3.0.0/FiraCode.zip -o /tmp/FiraCode.zip
unzip /tmp/FiraCode.zip -d ~/.local/share/fonts/
fc-cache -fv
{{ end }}# run_onchange_update-vim-plugins.sh
#!/bin/bash
# Runs when this script changes
vim +PluginInstall +qall# run_before_check-prerequisites.sh
#!/bin/bash
# Runs before chezmoi apply
if ! command -v git &> /dev/null; then
echo "Git is required but not installed!"
exit 1
fi
# run_after_restart-services.sh
#!/bin/bash
# Runs after chezmoi apply
systemctl --user daemon-reload
systemctl --user restart some-serviceOnly deploy certain files on certain machines:
# .chezmoiignore
{{ if ne .chezmoi.hostname "work-laptop" }}
.config/work/
{{ end }}
{{ if ne .chezmoi.os "darwin" }}
.config/macos/
{{ end }}Alternative to Gopass - use Bitwarden:
# Install Bitwarden CLI
$ brew install bitwarden-cli # or other method
# In template
{{ (bitwarden "item" "item-id").login.password }}Use 1Password for secrets:
# In template
{{ (onepasswordRead "op://vault/item/field") }}Include files from URLs:
# .chezmoiexternal.toml
[".vim/autoload/plug.vim"]
type = "file"
url = "https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim"
refreshPeriod = "168h" # 1 weekStore Chezmoi configuration in the repository:
# .chezmoi.toml.tmpl in repository
[data]
email = "{{ .email }}"
name = "{{ .name }}"
{{ if eq .chezmoi.hostname "work-laptop" }}
editor = "code"
{{ else }}
editor = "vim"
{{ end }}Complete example:
# private_dot_ssh/private_id_ed25519.tmpl
{{ output "gopass" "show" "-o" "ssh/personal_key" }}
# private_dot_ssh/id_ed25519.pub.tmpl
{{ output "gopass" "show" "-o" "ssh/personal_key_pub" }}
# private_dot_ssh/config.tmpl
Host github.com
IdentityFile ~/.ssh/id_ed25519
{{ if stat (joinPath .chezmoi.homeDir ".local/share/gopass") }}
# Gopass available - enhanced config
{{ end }}# dot_gitconfig.tmpl
[user]
name = {{ .name }}
email = {{ .email }}
[credential]
helper = store
[github]
user = {{ .github_username }}
# dot_git-credentials.tmpl
{{ $github_token := output "gopass" "show" "-o" "github/token" -}}
https://{{ .github_username }}:{{ $github_token }}@github.com# private_dot_aws/credentials.tmpl
[default]
aws_access_key_id = {{ output "gopass" "show" "-o" "aws/access_key_id" }}
aws_secret_access_key = {{ output "gopass" "show" "-o" "aws/secret_access_key" }}
[work]
aws_access_key_id = {{ output "gopass" "show" "-o" "work/aws/access_key_id" }}
aws_secret_access_key = {{ output "gopass" "show" "-o" "work/aws/secret_access_key" }}# dot_zshenv.tmpl
# Environment variables loaded before .zshrc
{{ if stat (joinPath .chezmoi.homeDir ".local/share/gopass") }}
# API Keys from Gopass
export OPENAI_API_KEY="{{ output "gopass" "show" "-o" "api/openai" }}"
export ANTHROPIC_API_KEY="{{ output "gopass" "show" "-o" "api/anthropic" }}"
{{ end }}# private_dot_config/systemd/user/backup.service.tmpl
[Unit]
Description=Automated Backup Service
[Service]
Type=oneshot
Environment="BACKUP_KEY={{ output "gopass" "show" "-o" "backup/encryption_key" }}"
ExecStart=/usr/local/bin/backup.sh
[Install]
WantedBy=default.target# recipe.yml
name: dev-workstation
base-image: ghcr.io/ublue-os/bluefin-dx-main
image-version: 40
modules:
- type: rpm-ostree
install:
# Development tools
- vim
- tmux
- git-delta
- ripgrep
- bat
- fd-find
- jq
- httpie
# Container tools
- podman
- buildah
- skopeo
# System tools
- htop
- ncdu
- tree
- wget
- type: default-flatpaks
system:
install:
- com.visualstudio.code
- com.slack.Slack
- us.zoom.Zoom
- org.mozilla.firefox# recipe.yml
modules:
- type: rpm-ostree
install:
- firejail # Sandboxing
- fail2ban # Intrusion prevention
- aide # File integrity
- lynis # Security auditing
- usbguard # USB device control
- type: files
files:
# Harden SSH
- source: ssh/sshd_config
destination: /etc/ssh/sshd_config.d/hardening.conf
# Kernel hardening
- source: sysctl/hardening.conf
destination: /etc/sysctl.d/99-hardening.confExample config/files/sysctl/hardening.conf:
# Kernel hardening parameters
kernel.dmesg_restrict = 1
kernel.kptr_restrict = 2
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.tcp_syncookies = 1# recipe.yml
modules:
- type: rpm-ostree
install:
- ffmpeg
- pipewire
- wireplumber
- type: default-flatpaks
system:
install:
- org.kde.kdenlive # Video editing
- org.gimp.GIMP # Image editing
- org.audacityteam.Audacity # Audio editing
- org.blender.Blender # 3D modeling
- com.obsproject.Studio # StreamingSimplify common tasks:
# Install just
$ sudo rpm-ostree install just
# Create justfile in your home directory
$ chezmoi add ~/.justfileExample .justfile:
# Update entire system
update:
rpm-ostree upgrade
flatpak update -y
chezmoi update
gopass sync
# Backup everything
backup:
#!/bin/bash
cd ~/.local/share/gopass/stores/root && git push
cd ~/.local/share/chezmoi && git push
echo "Backup complete"
# Full system check
check:
rpm-ostree status
gopass ls
chezmoi managedUse Nix for additional packages:
# Install Nix (in your custom image)
# Add to recipe.yml scripts
# Or use home-manager (Nix-based dotfiles manager)
# Alternative to Chezmoi if you prefer NixUse Ansible for complex setup:
# run_once_ansible-setup.sh
#!/bin/bash
if ! command -v ansible &> /dev/null; then
pip install --user ansible
fi
ansible-playbook ~/.config/ansible/setup.ymlDefine your infrastructure as code alongside your workstation:
# infrastructure/main.tf
resource "github_repository" "dotfiles" {
name = "dotfiles"
description = "My dotfiles"
visibility = "private"
}
resource "github_repository" "gopass_store" {
name = "gopass-store"
description = "Encrypted password store"
visibility = "private"
}Only install packages on certain machines:
# recipe.yml
modules:
- type: script
scripts:
- conditional-packages.sh# config/scripts/conditional-packages.sh
#!/bin/bash
HOSTNAME=$(hostname)
if [[ "$HOSTNAME" == "work-laptop" ]]; then
rpm-ostree install \
openvpn \
networkmanager-openvpn
fimodules:
- type: rpm-ostree
install:
- gnome-tweaks
- gnome-extensions-app
- type: default-flatpaks
system:
install:
- org.gtk.Gtk3theme.Adwaita-dark# In Chezmoi: run_once_configure-theme.sh
#!/bin/bash
# Set dark theme
gsettings set org.gnome.desktop.interface gtk-theme 'Adwaita-dark'
gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark'
# Install GNOME extensions
# ...Set up toolbox containers automatically:
# run_once_create-toolboxes.sh
#!/bin/bash
# Create development toolboxes
toolbox create -d fedora:40 development
toolbox create -d ubuntu:22.04 ubuntu-dev
# Python development container
podman pull python:3.12# private_dot_local/bin/executable_backup.sh.tmpl
#!/bin/bash
# Automated backup script
BACKUP_KEY="{{ output "gopass" "show" "-o" "backup/encryption_key" }}"
BACKUP_DEST="{{ output "gopass" "show" "-o" "backup/destination" }}"
# Backup important directories
restic backup \
--password-file=<(echo "$BACKUP_KEY") \
--repo="$BACKUP_DEST" \
~/.local/share/gopass \
~/.local/share/chezmoi \
~/Documents
# run_after_setup-backup-service.sh
systemctl --user enable --now backup.timer# Use layer caching effectively
modules:
# Install large, infrequently changing packages first
- type: rpm-ostree
install:
- kernel-devel
- gcc
- make
# Install frequently changing packages later
# This way changes don't invalidate early layers# Disable automatic sync for faster access
$ gopass config autosync false
# Sync manually when needed
$ gopass sync# Use --force to skip diff checking
$ chezmoi apply --force
# Only apply specific files
$ chezmoi apply ~/.zshrc ~/.gitconfig
# Use .chezmoiignore to skip large directories# .github/workflows/build.yml
strategy:
matrix:
variant: [main, nvidia]
max-parallel: 2 # Build multiple variants simultaneously# Store AGE key on hardware token (YubiKey)
# Requires age-plugin-yubikey
$ age-plugin-yubikey --generate > key.txt
# Use with Gopass
$ gopass config age.identities /path/to/yubikey/identity# Use YubiKey for GPG (alternative to AGE)
# Follow YubiKey GPG setup guide
# Store GPG key on hardware token# Use hardware token for SSH (PIV)
# YubiKey, SoloKey, etc.
# Generate key on token
$ ykman piv keys generate --algorithm ECCP256 9a /tmp/public.pem
# Use with SSH
$ ssh-add -s /usr/lib/libykcs11.so# Use systemd-homed for encrypted home
# Set up during installation or migrate later
$ homectl create username \
--disk-size=100G \
--storage=luks# Verify signed images before deployment
# In recipe.yml
modules:
- type: signing
type: signing
# Verify manually
cosign verify ghcr.io/username/bluefin-custom:latest- Bluefin: https://universal-blue.org/images/bluefin/
- BlueBuild: https://blue-build.org/learn/
- Gopass: https://github.com/gopasspw/gopass/tree/master/docs
- Chezmoi: https://www.chezmoi.io/
- AGE: https://age-encryption.org/
- Universal Blue Discord: https://discord.gg/universal-blue
- Fedora Discourse: https://discussion.fedoraproject.org/
- r/Fedora: https://reddit.com/r/Fedora
- r/linuxadmin: https://reddit.com/r/linuxadmin
Browse others' configurations for inspiration:
- Bluefin Images: https://github.com/ublue-os
- Awesome Chezmoi: https://github.com/chezmoi/awesome-chezmoi
- Dotfiles GitHub Topic: https://github.com/topics/dotfiles
- Fedora Silverblue/Kinoite: Understanding atomic desktops
- OCI Images: Container image concepts
- Git: Version control fundamentals
- YAML: Configuration syntax
- Shell Scripting: Automation
- yq: YAML processor - https://github.com/mikefarah/yq
- jq: JSON processor - https://stedolan.github.io/jq/
- bat: Better cat - https://github.com/sharkdp/bat
- ripgrep: Fast grep - https://github.com/BurntSushi/ripgrep
- fd: Fast find - https://github.com/sharkdp/fd
- just: Command runner - https://github.com/casey/just
- Age Specification: https://age-encryption.org/v1
- Sigstore/Cosign: Image signing - https://sigstore.dev/
- YubiKey Guide: https://github.com/drduh/YubiKey-Guide
Search for:
- "Fedora Silverblue setup"
- "Bluefin custom image"
- "Dotfiles management"
- "Infrastructure as Code workstation"
- "Immutable desktop Linux"
- Check documentation for the specific tool
- Search GitHub issues in relevant repositories
- Review example configurations
- Test in a VM or toolbox first
- GitHub Discussions: Project-specific questions
- Discord/Slack: Real-time community help
- Forums: Fedora Discourse, Reddit
- Stack Overflow: Technical problems
Good questions include:
- What you're trying to do
- What you've tried
- Error messages (complete, not snippets)
- Your configuration (relevant parts)
- Your environment (Fedora version, etc.)
When you solve a problem:
- Document it in your repository
- Share in community spaces
- Consider contributing to docs
- Help others with similar issues
The concepts you've learned in this workshop extend far beyond the specific tools:
- Declarative Configuration: Define desired state, not steps
- Version Control Everything: Git as the source of truth
- Automation: Reduce manual steps to zero
- Reproducibility: Identical results every time
- Testability: Try changes safely
- Documentation as Code: Configuration is self-documenting
These principles apply to:
- Workstations (what we covered)
- Servers
- Cloud infrastructure
- Network configuration
- Application deployment
- And more...
You now have a foundation to apply Infrastructure as Code principles to any computing environment.
Start small. Don't try to automate everything at once.
Begin with:
- Dotfiles (low risk, high value)
- Secrets (improve security)
- OS customization (when comfortable)
Iterate based on your needs. Add complexity only when it provides clear value.
The goal isn't perfection - it's improvement. Every configuration file you version control is progress. Every secret you encrypt properly is safer. Every manual step you automate is time saved.
Welcome to treating your workstation like production infrastructure.
Your laptop is cattle now. Enjoy the freedom.
Previous: Lab 6: Putting It All Together