Skip to content

Debian Setup ​

Personal Debian VPS dotfiles and setup scripts powered by apt, proto, and Oh My Zsh.

Prerequisites ​

The script requires either root or a user with sudo access. It will exit early with an error if neither is available.

PrerequisiteNotes
sudo or rootRequired to install packages and write to system directories
curlUsed to bootstrap Oh My Zsh, proto, Helm, and k9s
gitInstalled automatically via apt in the common step

Installation ​

Remote (fresh server) ​

sh
# Full install — all profiles, dotfiles, completions
bash <(curl -fsSL https://raw.githubusercontent.com/KevinDeBenedetti/dotfiles/main/os/debian/init.sh) -a

The script clones the repository to ~/.dotfiles if not already present, then re-executes from there.

Local ​

sh
# Clone
git clone https://github.com/KevinDeBenedetti/dotfiles.git ~/.dotfiles
cd ~/.dotfiles

# Full install
./os/debian/init.sh -a

# Dotfiles only
./os/debian/init.sh -d

# Specific profiles + completions
./os/debian/init.sh -p "base,kubernetes" -c

Flags ​

FlagDescription
-aFull install: all profiles + dotfiles + completions + cleanup
-p <profiles>Comma-separated list of profiles (e.g. base,kubernetes)
-dLink dotfiles into $HOME
-cInstall zsh CLI completions
-lLite mode — skip optional/heavy packages
-rRemove bootstrap temp directory after install
-hPrint help

At least one flag is required. Running without flags prints help and exits.

Common Packages ​

Installed unconditionally before any profile (via apt):

ca-certificates, curl, gnupg, gzip, jq, unzip, wget, xz-utils, git, build-essential

Oh My Zsh is also installed at this stage if ~/.oh-my-zsh does not exist.

Profiles ​

base ​

Core CLI tools installed via apt. Split into lite (always) and additional (full mode only).

Lite:

ToolDescription
Docker CEDocker engine from the official upstream repository
docker-ce, docker-ce-cli, containerd.ioCore engine, CLI client, and container runtime
docker-buildx-plugin, docker-compose-pluginBuildKit and Compose v2 plugins
ansibleAgentless configuration management and provisioning
fzfFuzzy finder for shell history, files, and more
sshOpenSSH client
treeDirectory tree visualizer
watchRepeat a command at intervals
yqPortable YAML/JSON/TOML processor
rsyncIncremental file transfer
TerraformInfrastructure as code, from HashiCorp's apt repository
Git Credential ManagerHTTPS Git credential helper behind credential.helper = manager

Docker CE is installed from https://download.docker.com/linux/debian using the official GPG key. The calling user is automatically added to the docker group.

On a node managed by the infra repository, the script leaves Docker out. It recognises such a node by the sshd drop-in that repository writes, /etc/ssh/sshd_config.d/00-infra-hardening.conf, the same marker that makes it skip SSH hardening. Those nodes run k3s with its own containerd, and their Ansible removes Docker, whose iptables rules forward published ports around the host firewall.

Git Credential Manager comes from its latest GitHub release (.deb, amd64 or arm64), checked against the SHA-256 digest GitHub publishes for the file. Linux has no default credential store, so when none is configured the script sets credential.credentialStore cache in the system config (/etc/gitconfig): credentials stay in memory (15 minutes by default), never on disk.

Additional (full mode):

ToolDescription
ghGitHub CLI — PRs, issues, repos from the terminal
nmapNetwork exploration and port scanning
protoMulti-language toolchain version manager (installed via curl if not present)

security ​

Hardens the system using a layered approach. Activated with -p security or included in -a (full install).

Environment variables ​

Set these before invoking the script to customize security behaviour:

VariableDefaultDescription
SSH_PORT22TCP port sshd listens on
SSH_ALLOWED_USERS(all)Space-separated list of users permitted to log in via SSH
HARDEN_TMPfalseRemount /tmp as a noexec tmpfs capped at 1G (opt-in)
sh
# Example: custom port + restrict SSH to a single user
SSH_PORT=2222 SSH_ALLOWED_USERS="deploy" bash <(curl -fsSL ...) -a

SSH hardening (/etc/ssh/sshd_config.d/00-hardening.conf) ​

SettingValueEffect
Port$SSH_PORTConfigurable port (default: 22)
PermitRootLoginnoDisable direct root login
PasswordAuthenticationnoKey-only authentication
MaxAuthTries3Block brute-force attempts
X11ForwardingnoNo X11 tunnelling
AllowTcpForwardingnoNo arbitrary port forwarding
ClientAliveInterval300Disconnect after 5 min idle
AllowUsers$SSH_ALLOWED_USERSRestrict login to named users (optional)

sshd keeps the first value it reads and loads drop-ins in lexical order, so the file is named 00- to win over others such as cloud-init's 50-cloud-init.conf. After writing it, the script checks the effective configuration (sshd -T) and warns if PermitRootLogin or PasswordAuthentication is still overridden.

UFW firewall ​

Default policy: deny all incoming, allow all outgoing. Rules added:

RuleProtocolComment
$SSH_PORT/tcpTCPSSH (respects custom port)
80/tcpTCPHTTP
443/tcpTCPHTTPS

WARNING

Docker writes its own iptables rules ahead of UFW's, so UFW does not filter ports published by containers. When Docker is installed, the script sets "ip": "127.0.0.1" in /etc/docker/daemon.json: -p 5432:5432 then binds to loopback only. Publish a service on purpose with an explicit address (-p 0.0.0.0:443:443).

Fail2Ban (/etc/fail2ban/jail.local) ​

SettingValue
bantime1 hour
findtime10 minutes
maxretry3 attempts
backendsystemd
banactionufw

Sysctl hardening (/etc/sysctl.d/99-security.conf) ​

Network:

ParameterValueEffect
net.ipv4.tcp_syncookies1SYN flood protection
net.ipv4.tcp_rfc13371TIME_WAIT assassination protection
net.ipv4.ip_forward0No IP forwarding
net.ipv4.conf.all.accept_redirects0Ignore ICMP redirects (MITM)
net.ipv4.conf.all.rp_filter1Reverse path filtering (anti-spoof)
net.ipv6.conf.all.accept_ra0Disable IPv6 router advertisements

Kernel:

ParameterValueEffect
kernel.kptr_restrict2Hide kernel pointer addresses in /proc
kernel.dmesg_restrict1Restrict kernel log access to root
kernel.randomize_va_space2Full ASLR enabled
kernel.yama.ptrace_scope1Restrict ptrace to parent processes
net.core.bpf_jit_harden2Harden BPF JIT compiler
vm.mmap_min_addr65536Prevent null pointer dereference exploits

Filesystem:

ParameterValueEffect
fs.suid_dumpable0No core dumps for setuid binaries
fs.protected_symlinks1Protect symlinks in world-writable sticky dirs
fs.protected_hardlinks1Only owner can follow hardlinks
fs.protected_fifos2Restrict FIFO creation in sticky directories
fs.protected_regular2Restrict regular file creation in sticky directories

Shared memory & /tmp (/etc/fstab) ​

Mount pointOptions
/dev/shmnoexec,nosuid,nodev
/tmpnoexec,nosuid,nodev,size=1G

The /tmp entry is only added with HARDEN_TMP=true: noexec on /tmp breaks some apt/dpkg maintainer scripts and builds, and the 1G cap is wrong for small servers.

AppArmor ​

Installs apparmor and apparmor-utils, adds GRUB boot parameters (apparmor=1 security=apparmor), enables the service, and sets all loaded profiles to enforce mode.

CrowdSec (full mode only) ​

Collaborative threat-intelligence IPS. Downloads the install.crowdsec.net repository script to a file (aborting on HTTP errors), runs it, and adds:

  • crowdsec-firewall-bouncer-iptables — automatic firewall banning
  • Collections: crowdsecurity/linux, crowdsecurity/sshd

Unattended upgrades ​

Configures automatic security patches only (${distro_id}:${distro_codename}-security). Auto-reboot is disabled.

Security audit tools (full mode only) ​

ToolDescription
lynisSystem and security auditing
rkhunterRootkit scanner
chkrootkitAnother rootkit checker
auditdKernel audit framework

kubernetes ​

Tools for Kubernetes cluster management.

Lite:

ToolDescription
kubectlKubernetes command-line client (installed from the official pkgs.k8s.io repo, currently v1.32)

Additional (full mode):

ToolDescription
helmKubernetes package manager (official get-helm-3 script, downloaded first)
k9sTerminal UI for Kubernetes clusters (latest release, SHA-256 verified)

Dotfiles Linking ​

Running -d symlinks (or copies) config files from the repository into $HOME. Any existing file is backed up with a datestamp (e.g. ~/.zshrc.bak.20260311) before being replaced.

SourceTargetMethod
config/zsh/.zshrc~/.zshrccopy¹
config/git/.gitconfig~/.gitconfigsymlink
config/git/ignore~/.config/git/ignoresymlink
config/proto/.prototools~/.proto/.prototoolssymlink
config/oh-my-zsh/*.zsh-theme~/.oh-my-zsh/custom/themes/symlink
config/shell/*~/.config/dotfiles/symlink
config/ssh/config~/.ssh/configsymlink
config/claude/settings.json~/.claude/settings.jsonsymlink
config/claude/commands/*~/.claude/commands/symlink
config/claude/hooks/*~/.claude/hooks/symlink
config/commands/*.md~/.claude/commands/symlink
config/commands/*.md~/.config/opencode/commands/symlink
config/commands/*.md~/.config/Code/User/prompts/*.prompt.mdsymlink²
config/skills/*/~/.claude/skills/symlink
config/opencode/opencode.json~/.config/opencode/opencode.jsonsymlink
config/copilot/hooks/*~/.copilot/hooks/symlink
config/ai-tools/~/.config/ai-toolssymlink
scripts/*/* (executables)~/.local/bin/symlink
config/ollama/~/.config/ollamasymlink
config/vscode/settings.json~/.config/Code/User/settings.jsonsymlink²
config/vscode/mcp.json~/.config/Code/User/mcp.jsonsymlink²

¹ .zshrc is copied rather than symlinked so machine-specific patches can be applied without affecting the tracked file.

² Only when VS Code is installed: settings.json and mcp.json need the code CLI on PATH, prompt files need the VS Code User directory to exist.

Local override stubs ​

The -d step also creates the following stub files if they do not already exist. They are never tracked by git and always take precedence:

FilePurpose
~/.zshrc.localMachine-specific zsh aliases, exports, and path additions
~/.gitconfig.localMachine-specific git overrides (e.g. user.email, signingkey)
~/.config/dotfiles/env.local.shMachine-specific environment variable overrides (no API keys)

SSH allowed signers ​

If ~/.ssh/id_rsa.pub exists, the script automatically creates ~/.ssh/allowed_signers for local SSH commit signature verification.

Completions ​

Running -c installs zsh completions for:

ToolSource
ghgh completion -s zsh
protoproto completions --shell zsh
uvuv generate-shell-completion zsh
zsh-completionsCloned into Oh My Zsh custom plugins and added to fpath

Completions are written to /usr/local/share/zsh/site-functions (with sudo when not root).

Zsh Configuration ​

The .zshrc at config/zsh/.zshrc is copied to ~/.zshrc so machine-specific patches can be applied without affecting the tracked file.

History ​

SettingValueEffect
HISTSIZE10000Number of commands kept in memory per session
SAVEHIST10000Number of commands persisted to ~/.zsh_history
HIST_STAMPSyyyy-mm-ddDate prefix on every history entry
HISTFILE~/.zsh_historyPersistent history file location

Theme ​

A custom Oh My Zsh theme kevin-de-benedetti is linked from config/oh-my-zsh/ into ~/.oh-my-zsh/custom/themes/ and set via ZSH_THEME.

Plugins ​

The following Oh My Zsh plugins are enabled in .zshrc:

PluginWhat it provides
aliasesals command — lists all active aliases with descriptions
colored-man-pagesANSI colour for man pages — much easier to read
dockerDocker CLI completion and aliases (dbl, dcin, dps, etc.)
docker-composedocker compose completion and shorthand aliases (dcup, dcdown, etc.)
ghGitHub CLI (gh) shell completion
gitExtensive Git aliases (gst, gco, glog, gcmsg, etc.) and branch in prompt
gitignoregi <lang> — fetches a .gitignore template from gitignore.io
rsyncAliases for common rsync patterns (rsync-copy, rsync-move, etc.)
sudoDouble-press ESC to prepend sudo to the current or previous command

fzf Integration ​

When installed (via the base profile), fzf provides the following shell key bindings:

BindingAction
CTRL-TFuzzy-search files/directories and paste the path
CTRL-RFuzzy-search command history and run selected entry
ALT-CFuzzy cd into a subdirectory

Local Overrides ​

The following files are sourced at the end of .zshrc if they exist. They are never tracked by git:

FilePurpose
~/.config/dotfiles/env.local.shMachine-specific environment variables (no API keys)
~/.zshrc.localMachine-specific aliases, path additions, and exports

Shell Functions ​

Custom functions live in config/shell/functions.sh and are sourced automatically by .zshrc. Run lsfn to list all functions with their help text.

FunctionUsageDescription
b64db64d <string>Decode a base64 string
b64eb64e <string>Encode a string to base64
browserbrowser [-- <url>]Start a Browsh terminal browser via Docker
cheat_glowcheat_glow <sheet>Render a cheat cheatsheet through glow at 150 columns for readability
check_certcheck_cert <url>Print TLS certificate details for a domain using curl
dksdks <secret> [namespace]Decode a Kubernetes secret — outputs all .data values base64-decoded via yq
kbpkbp <port>Kill the process currently listening on the given TCP port
randompassrandompass [length]Generate a secure random password (default 24 chars) with mixed complexity
timestampdtimestampd <unix_ts>Convert a Unix timestamp to a human-readable date (cross-platform)
timestampetimestampe <YYYY-mm-ddTHH:MM:ss>Convert a date string to its Unix timestamp (cross-platform)
vpnvpn <up|down|status|list> [tunnel]Manage WireGuard tunnels via wg-quick (aliases: vup, vdown, vpns)

Run any function with -h or --help to see its usage message, e.g. dks -h.

Git Configuration ​

config/git/.gitconfig is symlinked to ~/.gitconfig. Key settings:

SectionSettingValueEffect
useruseConfigOnlytrueIdentity must come from a config file, never guessed
pushdefaultsimplePush to the tracking branch only (safe default)
pullrebasetrueAlways rebase on pull instead of merge
branchautosetuprebasealwaysNew branches track their remote with rebase
rerereenabledtrueRecord and replay conflict resolutions automatically
commitgpgsigntrueSign every commit with your SSH key
taggpgSigntrueSign every tag with your SSH key
gpgformatsshUse SSH key (not GPG keyring) for signing
gpg.sshallowedSignersFile~/.ssh/allowed_signersFile that maps emails to trusted public keys
initdefaultBranchmainNew repositories default to main
credentialhelpermanagerHTTPS auth through Git Credential Manager³
urlinsteadOfgit@github.com:GitHub SSH remotes are rewritten to HTTPS (GCM)

³ Git Credential Manager is installed by the base profile; without it, git warns that credential-manager is not a git command and falls back to prompting. Details in Git.

Machine-specific overrides ​

Create ~/.gitconfig.local — it is [include]d at the bottom of .gitconfig and is never tracked by git:

ini
# ~/.gitconfig.local
[user]
  email = server@yourdomain.com
  signingkey = ~/.ssh/id_ed25519_server.pub

SSH commit signing ​

When ~/.ssh/id_rsa.pub exists the dotfiles install creates ~/.ssh/allowed_signers automatically. This allows Git to verify signed commits locally with git log --show-signature.

SSH Client Configuration ​

config/ssh/config is symlinked to ~/.ssh/config. The global Host * block applies to every connection:

DirectiveValueEffect
SendEnv -LC_* -LANG(off)Do not forward locale variables — avoids setlocale warnings on servers
ControlMasterautoReuse an existing connection if one is already open
ControlPath~/.ssh/cm-…Socket path for multiplexed connections
ControlPersist60sKeep the master connection open for 60 s after the last session closes
ServerAliveInterval60Send a keepalive packet every 60 s
ServerAliveCountMax3Drop the connection after 3 missed keepalives (~3 min)

proto Toolchain ​

config/proto/.prototools is symlinked to ~/.proto/.prototools. It pins global tool versions and configures proto's behaviour:

toml
bun   = "latest"
node  = "latest"
npm   = "bundled"   # ships with the Node.js version
pnpm  = "latest"

[tools.node]
bundled-npm = true  # keep npm in sync with node

[tools.npm]
shared-globals-dir = true  # global npm packages in a single shared dir

[settings]
auto-install  = true     # install missing tool versions automatically
auto-clean    = true     # remove unused tool versions after upgrades
pin-latest    = "global" # record the resolved "latest" version globally

Managing tool versions ​

sh
# Install / pin a specific version
proto install node 22

# Use a version in the current directory only
proto use python 3.12 --local

# List all tools managed by proto
proto list

# Upgrade proto itself
proto upgrade

Useful apt Commands ​

sh
# Refresh package index
sudo apt-get update

# Upgrade all installed packages
sudo apt-get upgrade

# Install a package without recommended extras
sudo apt-get install -y --no-install-recommends <package>

# Search for a package
apt-cache search <keyword>

# Remove a package and its config
sudo apt-get purge <package> && sudo apt-get autoremove

Common Issues ​

sudo not available on a fresh VPS:

sh
# As root, install sudo and add your user
apt-get install -y sudo
usermod -aG sudo <username>
# Then log out and back in

Docker CE daemon not started after install:

sh
sudo systemctl enable docker
sudo systemctl start docker
# Log out and back in for group membership to take effect
# (the script already runs: usermod -aG docker $USER)

Docker GPG key or repository error:

sh
# Re-add the GPG key manually
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg \
  | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

kubectl not found after install:

sh
# Verify the apt source was added correctly
cat /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update && sudo apt-get install -y kubectl