Setting Up a New Machine - uhop/dotfiles GitHub Wiki

Setting up a new machine

One-time steps to bring a vanilla machine into this project. Once set up, day-to-day usage is covered by the Workflows pages.

Quick path A โ€” zero setup on the target (curl-pipe)

If you can get a shell on the new machine (physically or via SSH) and it has network access, the fastest path is the authoritative bootstrap script hosted in this repo:

curl -fsSL https://raw.githubusercontent.com/uhop/dotfiles/main/install.sh | sh

Add --yes for unattended / CI use (no prompts):

curl -fsSL https://raw.githubusercontent.com/uhop/dotfiles/main/install.sh | sh -s -- --yes

The script installs OS prerequisites (curl + git + bash-4 + build tools), Homebrew, chezmoi, then runs chezmoi init --apply uhop/dotfiles. Requires an account with passwordless sudo (or root). Takes 5โ€“15 minutes depending on network. Safe to re-run.

Quick path B โ€” from an existing operator machine

If you already have a machine with this project, bootstrap-dotfiles is a thin wrapper that pipes install.sh over SSH and runs it unattended on the target:

# 1. Set up SSH access (dry-run first, then --apply)
bootstrap-remote newhost
bootstrap-remote newhost --apply

# 2. SSH + curl-pipe in one command
bootstrap-dotfiles newhost

To bootstrap the local machine and pull secrets from your jot bundle in one go:

# Pre-stage ~/.env + ~/.ssh/age.key from another machine or backup, then:
bootstrap-dotfiles --from-jot my-bundle-prefix

The rest of this page documents what happens under the hood and what to do when the automated paths don't work.

SSH key setup

Passwordless SSH is a prerequisite for remote workflows and playbash. This project uses key-based authentication (public/private key pairs, not CA-signed SSH certificates).

bootstrap-remote automates key distribution and hardening (see below). To do it manually:

Generating a key pair

If you don't have one:

# Ed25519 (preferred โ€” short, fast, modern)
ssh-keygen -t ed25519 -C "[email protected]"

# RSA 4096 (fallback for older systems that don't support Ed25519)
ssh-keygen -t rsa -b 4096 -C "[email protected]"

Accept the default location (~/.ssh/id_ed25519 or ~/.ssh/id_rsa). Use a passphrase for interactive machines; skip it for dedicated automation hosts where an agent can't prompt.

On macOS, add the key to the system keychain so it persists across reboots:

ssh-add --apple-use-keychain ~/.ssh/id_ed25519

Then add to ~/.ssh/config (inside config.d/ โ€” see SSH client configuration):

Host *
  AddKeysToAgent yes
  UseKeychain yes

On Linux, this project installs ssh-agent as a systemd user service (see After setup: what's running below and Shell Environment ยง SSH agent (Linux)). Once dotfiles are applied, the agent starts automatically at login โ€” no eval "$(ssh-agent -s)" needed. Add keys with:

ssh-add ~/.ssh/id_ed25519

Keys stay loaded until logout.

Distributing keys to remote hosts

Copy your public key to each host you want to access:

ssh-copy-id user@host

This appends your public key to ~/.ssh/authorized_keys on the remote host. You need password access for this one-time step.

This project also auto-populates ~/.ssh/authorized_keys on every chezmoi-managed machine by pulling your public keys from GitHub (via the GITHUB_USERNAME chezmoi variable). So once a machine has chezmoi, any key you've uploaded to GitHub grants access automatically.

Distributing keys across your own computers

To use the same identity from multiple machines (e.g., laptop + desktop), copy the private key:

# From machine A to machine B
scp ~/.ssh/id_ed25519 ~/.ssh/id_ed25519.pub user@machineB:~/.ssh/

Then set permissions on machine B:

chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub

Alternatively, generate a separate key pair on each machine and add all public keys to GitHub โ€” chezmoi will distribute them to authorized_keys on every managed host.

Sensitive files (secrets, credentials)

Files like ~/.env, ~/.msmtprc, and ~/.ssh/age.key contain secrets that can't live in git. When machines can reach each other, scp or playbash put works. When they can't, use jot as a secure intermediary โ€” files are encrypted and stored on S3.

Once the encryption key and AWS credentials (~/.env) are on the new machine, pull everything else with jot get:

jot get sys/msmtprc && mv msmtprc ~/.msmtprc && chmod 600 ~/.msmtprc

See Jot โ€” Sharing sensitive files for naming conventions and the full workflow.

SSH server hardening

After key-based access is working, disable password authentication and root login on each remote host. Test your key access first โ€” getting this wrong locks you out.

# Verify key access works BEFORE changing anything
ssh user@host "echo ok"

Then on the remote host:

# Edit sshd_config (use sudoedit if available)
sudoedit /etc/ssh/sshd_config

Change or add these lines:

PasswordAuthentication no
PermitRootLogin no

Restart the SSH daemon:

# Linux (systemd)
sudo systemctl restart sshd

# macOS
sudo launchctl stop com.openssh.sshd
# (macOS restarts it automatically)

Verify by trying to connect with a password from another terminal (it should be refused) while keeping your existing session open as a safety net.

Hardening checklist

  • Key-based login works for your user
  • PasswordAuthentication no in /etc/ssh/sshd_config
  • PermitRootLogin no in /etc/ssh/sshd_config
  • sshd restarted
  • Password login rejected (tested from a separate session)
  • Other users who need access have their keys in authorized_keys

SSH client configuration

This project deploys ~/.ssh/config with:

Include config.d/*

Host *
  ControlMaster auto
  ControlPath ~/.ssh/sockets/%r@%h-%p
  ControlPersist 5m

Connection multiplexing is on by default: the first connection to a host opens a control socket; subsequent connections reuse it. This speeds up playbash fan-out (no repeated handshakes) and makes scp/sftp after ssht instant.

Per-host configuration

Add host-specific settings in ~/.ssh/config.d/ (one file per host or group):

# ~/.ssh/config.d/servers
Host srv1
  HostName 192.168.1.10
  User eugene
  IdentityFile ~/.ssh/id_ed25519

Host srv2
  HostName 192.168.1.11
  User eugene

Host *.internal
  ProxyJump bastion

These host aliases are used directly by ssht, playbash, and all other SSH tools. The playbash inventory references the same host names โ€” SSH-level overrides (user, port, key, proxy) belong here, not in the inventory.

Installing the dotfiles

Automated (from another machine)

If you have an existing machine with this project installed, use bootstrap-dotfiles to prepare the new host:

bootstrap-dotfiles newhost

This detects the remote OS, generates a tailored install script, and uploads it. Then follow the printed instructions โ€” SSH into the new host and run the script:

ssht newhost
/tmp/install-dotfiles.sh

The script handles prerequisites, brew, and chezmoi init --apply. It will ask for sudo password and chezmoi configuration (name, email, GitHub username, GUI). At the end it offers to reboot.

Self-bootstrap (no SSH access to an operator)

When the new machine can't reach an existing operator machine over SSH (different network, NAT, behind a firewall), use --from-jot directly on the new host. It pulls system files from S3 via jot-deploy instead of copying them from the operator.

# On the new machine (after ~/.env and ~/.ssh/age.key arrive out-of-band):
bootstrap-dotfiles --from-jot sys/myhost

Pre-flight requires:

  • ~/.env โ€” AWS credentials (S3 access).
  • ~/.ssh/age.key (or any of ~/.ssh/id_ed25519, ~/.ssh/id_rsa, ~/.ssh/id_ecdsa) โ€” decryption key for the bundle.

These two files are the irreducible "chicken-and-egg" โ€” they must arrive on the new machine via USB, a password manager, or scp from a reachable host before --from-jot can do anything. See Jot ยง Bootstrap sequence.

Once the script runs:

  1. Installs prerequisites + Homebrew.
  2. Installs chezmoi and runs chezmoi init --apply.
  3. Runs jot-deploy <prefix> (report-only first), then prompts to --apply.
  4. Offers to reboot.

Manual

See the README for manual steps. Summary for Ubuntu/Debian:

sudo apt install build-essential curl git git-gui gitk micro
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
brew install chezmoi
chezmoi init --apply --recurse-submodules=false uhop

For Red Hat-like distros (Fedora, RHEL, CentOS, Rocky, Alma):

sudo dnf install gcc gcc-c++ make curl git git-gui gitk unzip
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
brew install chezmoi
chezmoi init --apply --recurse-submodules=false uhop

Reboot after chezmoi init --apply. The chezmoi run scripts install system packages (via apt or dnf), brew packages, nvm, bun, tmux plugins, and write /etc/sudoers.d/chezmoi for passwordless maintenance commands. doas is not installed by default; install opendoas manually if you prefer it โ€” see Application Notes.

See Platform Notes for additional Ubuntu post-install steps (fonts, video codecs, clipboard tools) and macOS specifics (switching the default shell to bash). For per-distro support tiers, known quirks, and troubleshooting when the install hangs or fails, see Distro Compatibility.

After setup: what's running

A glance-able overview of what lands on a machine after chezmoi init --apply finishes โ€” so there are no surprises. Each row links to the file or wiki page with full details.

Services enabled (Linux)

Service Scope Purpose Source
ssh-agent.service + .socket user (systemd) Socket-activated ssh-agent; one per login, listens at $XDG_RUNTIME_DIR/ssh-agent.socket. Overrides any distro-shipped unit so systemd user env publishes the right SSH_AUTH_SOCK. See Shell Environment ยง SSH agent (Linux). dot_config/systemd/user/ssh-agent.{service,socket}
[email protected] user (systemd) Template unit used by periodic task notification wrappers โ€” emails journalctl context on unit failure. Instantiated per failing unit, not always-on. dot_config/systemd/user/[email protected]
Periodic tasks you add user (systemd) / launchd setup-periodic add ... creates timers for daily/weekly maintenance (playbash run @self daily --report etc.). None installed by default โ€” you opt in per host. See Workflows: maintenance ยง Scheduled maintenance. created on demand

On macOS the systemd units are not deployed (.chezmoiignore excludes .config/systemd on non-Linux). ssh-agent is provided by the OS Keychain / launchd integration; see SSH key setup above.

System configuration written

What Where Why
Passwordless sudo rules /etc/sudoers.d/chezmoi Lets maintenance commands (upd, dcm, restart docker/containerd, etc.) run without a password prompt. Platform-specific command set.
doas config (if installed) /etc/doas.conf Same idea via doas; aliased to sudo in shells. Opt-in: install opendoas to activate. See Application Notes ยง passwordless privilege escalation.
SSH authorized_keys ~/.ssh/authorized_keys Rendered from GitHub-registered keys for GITHUB_USERNAME. Any key you upload to GitHub grants access to every chezmoi-managed machine.
SSH client config ~/.ssh/config, ~/.ssh/config.d/ Connection multiplexing by default; per-host overrides go in config.d/. See SSH client configuration.
etckeeper (if installed) /etc/.git Tracks /etc in git. Commits happen on package install.

User environment

What Where Notes
Shell environment ~/.bashrc, ~/.bash_aliases Exports (EDITOR, FORCE_COLOR, GITHUB_USERNAME, JOT_AWS_S3_BUCKET), PATH, tool init, aliases. Full map: Shell Environment.
Per-machine overrides ~/.env User-owned, untracked. Secrets and per-machine values. See Shell Environment ยง .env loading.
Custom CLI utilities ~/.local/bin/ Every tool in Utilities plus their bash completions.
Package set brew + apt/dnf Installed by run_onchange_before_install-packages.sh.tmpl (system) and run_once_after_install-*.sh.tmpl (brew, nvm, bun, tmux plugins, options.bash).

Network ports / listeners

By default, nothing on this dotfiles setup opens a listening port. Any listeners are from apps you install separately (Docker Compose stacks, ttyd if you run it, etc.) โ€” not from chezmoi scripts.

If you want to disable something

  • ssh-agent user service: systemctl --user disable --now ssh-agent.socket ssh-agent.service. Remove the SSH_AUTH_SOCK export by editing dot_bashrc.tmpl (the {{ if eq .osFamily "linux" }} block) and re-applying.
  • Periodic tasks: setup-periodic list, then setup-periodic remove <name>.
  • Passwordless sudo: sudo rm /etc/sudoers.d/chezmoi (will be regenerated on next chezmoi apply of the relevant script โ€” remove the run_onchange_after_install-sudoers.sh.tmpl from apply via .chezmoiignore if permanent).

Manual steps

Settings the dotfiles leave to you, by choice:

  • A Mac that other machines connect to (ssh, playbash): turn off system sleep. Otherwise the Mac sleeps most of the day, and connections to it time out. For details, see Platform Notes ยง macOS.
    sudo pmset -a sleep 0

Post-setup verification

After reboot, verify the setup:

  • Shell prompt shows git branch info (bash-git-prompt is working)
  • fastfetch runs on terminal open
  • eza, bat, fzf, zoxide are available (ls, cat, Ctrl+R, z)
  • node --version returns a current version (nvm is working)
  • micro opens (editor is working)
  • git gui and gitk open (GUI git tools, if the machine has a GUI)
  • tmux starts; install plugins with Prefix+I on first run
  • playbash hosts lists your inventory (if applicable)
  • On Linux: systemctl --user status ssh-agent is active; echo $SSH_AUTH_SOCK resolves under /run/user/<uid>/
โš ๏ธ **GitHub.com Fallback** โš ๏ธ