- Shell 87.8%
- Jinja 12.2%
Quickstrap
SintesLab - Quickstrap exists with the purpose of bootstrapping a fresh Linux host over SSH, because I am lazy and don't want to do it myself. Now you can be lazy too.
Currently it works with Debian and Ubuntu-based distros. I promise, my mental //TODO has Fedora and RHEL listed. Arch, Gentoo, and LFS may come up after I retire.
When pointed at an IP address, Quickstrap creates an Ansible user, keygens and installs that user's SSH key, hardens sshd so password logins are 'Match' disabled, and optionally sets the hostname and timezone. Once a host has been bootstrapped, Quickstrap can be used to run Ansible playbooks, putting in even less effort than usual.
Who this is for:
- Homelabbers and systems administrators in small to medium businesses (depending on how you define business size), so essentially guys like me.
- People who want to read some bash and learn more about it (like myself).
Who this isn't for:
- Whoever looks at it and thinks "Nah, I don't need this."
- Anyone whose job description includes taking care of audits or being audited, or runs amounts of hosts which would lead to them caring about the Big O complexity of functions in a bash script, Terraform magicians, or other high-level wizards. You are absolutely welcome to use this, but I really believe you probably do not need to.
- People who take pride in personally logging onto every host. Mad respect.
A note on my comments:
Unless they are factually incorrect or confusing, assume them to be completely necessary to code execution. Especially the silly ones. Even more so the ones literally explaining functions in words so that even a complete $stupid_variable can skim them quickly. The goal is for the code to be relatively understandable to a new-ish Linux admin and also to the very forgetful and sometimes extremely absent-minded maintainer.
Copyright (C) 2026 SintesLab. Licensed under the GPL-3.0-or-later; see the license header in each script for details.
Contents
- Requirements
- Installation
- Quick start
- Directory layout
- Commands
- The config file
- What happens on the remote host
- A note on ansible
<- you will find all available playbooks here - Final words and security
- Frequently asked questions
- License
- AI Usage Disclosure
Requirements
Control machine (where you run Quickstrap): Linux, WSL, or macOS.
Quickstrap needs ssh, scp, and ssh-keygen on the machine you run
it from. You need to also have ansible if you wish to use ansible.
If you do have ansible and wish to use --run-ansible, you may also want to get fzf. It is entirely optional, but makes the interface look better.
Remote host: Debian or Ubuntu, reachable over SSH.
Installation
Clone or download this repo, unless you wish to copy it by hand.
CAUTION: the decision to cleanly retype the whole repo, by the use of a good keyboard, carries serious health risks such as "sudden ASMR".
Run ./install.sh once from the repository root. It makes the shell scripts
executable, sets the ownership of the tree on Linux and WSL (macOS is left as-is),
and symlinks quickstrap onto your PATH:
- Linux / WSL:
/usr/local/bin/quickstrap(requiresroot permissions) - macOS:
~/.local/bin/quickstrap
You can still run ./quickstrap directly from the repo without installing,
but running install.sh is the recommended way so that you don't even need to type ./,
or, even worse, XO bash.
Quick start
# Clone the repo
git clone https://codeberg.org/SintesLab/SintesLab-Quickstrap
# Install quickstrap onto your PATH
cd SintesLab-Quickstrap && ./install.sh
# Setup to pick an Ansible username, an SSH port, and a timezone.
# Choosing the timezone does not require you to use it later.
quickstrap --setup
# Bootstrap a single host. You'll be asked for the login user, whether to
# set a hostname and timezone (both optional), and the Ansible sudo password.
quickstrap --send xxx.xxx.x.xx
# Once the machine is bootstrapped, you can run Ansible playbooks
# against it (depending on the playbook, you may need to set variables first).
quickstrap --run-ansible
If you do not wish to use the scripts interactively, see the
[--send fast-track][#quickstrap---send---fast-track] flags below.
Should you have multiple hosts to set up quickly, see the
--super section further down.
It reads a quickstrap-super.ini file and runs --send with fast-track flags on
each one of them.
Directory layout
Quickstrap has (and expects) the following layout:
repo/
├── install.sh # run once to install onto your PATH
├── quickstrap # quickstrap dispatcher
├── uninstall.sh # generated by install.sh; removes the symlink
├── quickstrap-super.ini # optional, only needed for --super
├── quickstrap-super.example.ini
├── ansible/
│ ├── ansible.cfg
│ ├── alt_hostfiles/ # add any alternative hostfiles here
│ ├── playbooks/ # ansible playbooks
│ ├── roles/
│ ├── group_vars
│ │ └── all.yml # All variables ansible uses are here
│ ├── hosts.example.ini
│ └── ...
├── windows/ # Alloy config template and generator for Windows monitoring targets
├── alt_quickstrap-super/ # you can add alternative quickstrap-super.ini files here
└── dir-quickstrap/
├── setup.sh
├── send.sh
├── super.sh
├── ansible.sh
├── fast-track.sh
├── spinner.sh
├── sinteslab-banner.sh
├── payload-src/ # scripts staged and copied to the remote host
│ ├── strapme.sh
│ ├── progress-bar.sh
│ └── checkwinsize.sh
├── config.example
├── config # generated by --setup
├── keys/ # generated by --setup, holds SSH keypairs
├── backup/ # generated only if a key is regenerated
├── runtime/ # transient files during --send
└── history # timestamped log of every --send run (mode 600)
config, keys/, backup/, runtime/, and history don't exist until you use
Quickstrap. uninstall.sh appears after ./install.sh. These are written
next to the scripts rather than committed to the repository, since they hold
values, keys, and install state specific to your own machine. Ephemeral
password and key files written into payload-src/ during --send are also
kept out of version control.
Commands
quickstrap --setup
Runs an interactive wizard that asks for an Ansible username and an SSH
port, generates an ed25519 keypair for that username under keys/, and lets
you pick a timezone by browsing the local /usr/share/zoneinfo tree. The
result is written to a config file that other commands read from, so you
only need to answer these questions once, unless you wish to change the setup
between groups of machines.
If a key already exists for the username you choose, it's backed up to
backup/ before a new one is generated, rather than being silently
overwritten.
If /usr/share/zoneinfo isn't present or readable (this can happen on some
minimal systems), the wizard falls back to letting you continue with UTC or
exit so you can install tzdata first, rather than guessing.
quickstrap --send <ip>
Bootstraps a single host at the given IP address. Without any extra flags, it asks you interactively for the login user on the remote host, whether to set the hostname and timezone you chose during setup, and the Ansible sudo password.
Here is what happens once those questions are answered:
-
An SSH connection is opened once, using SSH's
ControlMasterfeature. The password prompt, the file copy, and the final session all reuse the same authenticated connection instead of asking you to authenticate three times. -
The Ansible password and the public key are written into
payload-src/just long enough to be copied to the remote host, then deleted from the local machine immediately afterward.The password file is created at mode 600 before anything is written to it so as not to be readable for other users logged on to your machine.
-
The payload is copied to
/tmp/qs-payloadon the remote host with permissions preserved. -
strapme.shruns on the remote host over a full pseudo-TTY. When done, it drops you into an interactive shell session on the remote host, in case you wish to do something extra or inspect the result / log.
Setting the hostname or the timezone on a remote host can break existing
infrastructure that depends on the current values, so --send always asks
for explicit confirmation before doing either. The only way to skip this
confirmation is using the --hostname, --timezone, --no-hostname, or
--no-timezone fast-track flags described below.
Run quickstrap --send --help (same bro.. same) to see the full list of fast-track flags.
--login-user=<user> SSH login user on the remote host. Defaults to root.
--ansible-user=<user> Ansible username to create. Also generates its keypair.
--ssh-port=<port> SSH port the remote host is currently listening on.
--timezone=<tz> IANA timezone to set on the remote host (e.g. Europe/Berlin).
--hostname=<hostname> Hostname to set on the remote host.
--no-hostname Skip setting the hostname on the remote host.
--no-timezone Skip setting the timezone on the remote host.
When any of --ansible-user, --ssh-port, and --timezone are provided,
a config file is not required. Defaults are stubbed in memory so you can run
--send without having run --setup first.
Every --send run appends a timestamped entry (date, target IP, and remote
arguments) to dir-quickstrap/history (created at mode 600 on first use),
for easy documentation of what was bootstrapped and when.
quickstrap --super <--single-pass|--multi-pass>
Runs --send in fast-track mode for every host listed in a
quickstrap-super.ini file, which must sit next to the quickstrap
script itself (see quickstrap-super.example.ini for the exact format).
You can have multiple quickstrap-super.ini files. Create as many .ini files
as you wish in the alt_quickstrap-super directory. Passing the --alt-file
argument after single-pass or multi-pass will let you pick one of your
.ini files from the alt_quickstrap-super dir interactively.
--super exists to make your life easier when you wish to bootstrap a
batch of hosts all at once, with the same ansible user and the same or
different ansible user sudo pass for each one.
If your login user has key-based ssh auth, the script runs without showing a single prompt in --single-pass mode. And, let's be honest, you should be using key-based auth anyways.
Each section of the INI file is an IP address, and the keys underneath it
are the same values --send accepts in the command line.
login_user, ansible_user, and ssh_port are required, while timezone and hostname
are optional.
Commenting out a section header (for example, #[1.2.3.4])
skips that host entirely.
If the same IP address appears twice, only the first occurrence is used.
In that case, a .ini.deduped copy of the file is written with the later,
duplicate section commented out and annotated, as I respect your choices
and wouldn't delete your section.
You must choose one of two ways to handle the Ansible sudo password:
--single-passasks for the password once and reuses it for every host in the file.--multi-passasks for a separate password for each host, all collected up front before any SSH connections are made.
Either way, the password for each host is passed to send.sh through the
ANSIBLE_QS_PASS environment variable, which it reads once and
unsets immediately afterward.
quickstrap --run-ansible
Opens an interactive picker over the .yml/.yaml files in
ansible/playbooks/, using fzf if it's installed or a numbered menu
otherwise.
After a playbook is chosen, you're shown a small menu of common
ansible-playbook flags (-K, --ask-vault-pass, --step, --limit, and
--hostfile) that can be toggled on or off before the playbook runs.
--hostfile lets you pick an alternate inventory file from ansible/alt_hostfiles/
instead of the default hosts.ini.
quickstrap --update
Pulls the latest code from the SintesLab-Quickstrap repo, re-runs
install.sh to refresh permissions and regenerate uninstall.sh/update.sh,
and shows the CHANGELOG.
This always gets you the current tip of main — including any commits that
haven't been tagged yet. Run it whenever quickstrap tells you a new release
is available, or anytime you just want the latest bleeding-edge code,
playbooks included, ahead of the newest tagged release.
Running quickstrap --update unprompted essentially gives you the "nightly"
version of quckstrap (..ahem.. not that tags can be called stable).
Update notifications
Every 24 hours, quickstrap quietly checks, when used while you have an
internet connection, whether a newer tagged release exists on the remote
(not just newer commits on main).
If you're behind, you'll see something like:
A quickstrap update is available (v1.1.0 -> v1.2.0, 4 commit(s) behind):
a1b2c3d feat: add windows target config generator playbook
9f8e7d6 fix: correct ufw rule ordering in prometheus-deb role
Run 'quickstrap --update' to get the latest code.
quickstrap --show-location
If you are goofy like me and forget where you cloned the repo after a while
--show-location will tell you where the Sinteslab-Quickstrap dir is on your
filesystem.
You can even do something like: cd $(quickstrap --show-location)
The config file
Once --setup has run, its answers live in dir-quickstrap/config as
plain KEY=value lines:
ANSIBLE_USERNAME=ansible
SSH_PORT=22
TIMEZONE=UTC
What happens on the remote host (strapme.sh)
strapme.sh is copied to the remote host as part of the payload and does
the actual bootstrapping work there, in seven steps:
- checking internet connectivity
- creating the Ansible user
- setting that user's password and immediately shredding the password file
- installing required packages (
python3,python3-apt,python3-debian,curl,git, andufwunlessiptables-persistentornetfilter-persistentis already present) - applying the hostname and timezone if requested
- installing the SSH authorized key
- hardening sshd so that only key-based authentication is accepted for that user
(written to a drop-in file under
sshd_config.d/on modern systems, or appended as aMatch Userblock tosshd_configon older ones; validated withsshd -tbefore reload)
Every step is logged to bootstrap.log inside the payload directory, and
that log is copied to /var/log/quickstrap-bootstrap.log on the remote host
before the payload directory is removed.
A note on ansible
I cannot provide a full guide on how to write and use Ansible, but here is a quick rundown of what is relevant to this repository:
ansible/
├── ansible.example.cfg
├── group_vars
│ └── all.example.yml
├── hosts.example.ini
├── playbooks
│ ├── grafana_loki_prometheus_deb.yml
│ ├── linux_server_alloy_setup_deb.yml
│ ├── system_updates-deb.yml
│ └── windows_target_config_generator.yml
└── roles
├── alloy-deb
├── alloy-docker-deb
├── base-deb
├── caddy-deb
├── grafana-deb
├── log
├── loki-deb
├── prometheus-deb
└── sudo-detect
-
group_vars/all.ymlis where every variable the playbooks and roles read from lives. If you want to change a port, a version, or a path, this is the first file to look at.all.example.ymlshows the format.!IMPORTANT: the default Ansible user name sits in this file as well, as well as the SSH key location for this user. You can create other host_group.yml files and place further ansible users and ssh key locations based on the host group you have created in your hosts.ini file.
-
ansible.cfgholds the usual Ansible settings: where the inventory lives, where the roles live, and a few defaults so you're not typing-i hosts.iniby hand every time. -
hosts.iniis the inventory: the list of hosts (and host groups) a playbook actually runs against. Seehosts.example.inifor the format. -
roles/holds the reusable building blocks. Each one follows the standard Ansible role layout (tasks/,handlers/,templates/, and sometimesdefaults/), and is meant to be pulled into one or more playbooks rather than run on its own. -
playbooks/holds the actual entry points: each.ymlfile wires together whichever roles it needs and applies them to a group of hosts from the inventory.
Note that I do not set the Ansible user to use passwordless sudo.
This means you need to use -K when running the playbooks unless you do so yourself.
If you decide to use password protected ssh keys it helps to have ssh-agent installed.
ssh-agent bash
ssh-add /whatever/location/your/key/has
exit # When done
You will get prompted way less often.
None of this replaces reading the actual .yml files before you point a
playbook at something you care about. Treat Ansible like a loaded gun,
especially knowing that I wrote it (just kidding, or AM I??!?).
Final words and security
I have taken some security precautions while making Quickstrap. Credential files are always created with restrictive permissions and cleaned up after having been used, SSH for the new user is hardened a bit, and SSH connections are closed after use.
None of these make Quickstrap a hardened security tool by any stretch of the imagination. It exists purely so that it is convenient to get an Ansible user on a host or small fleet quickly, after which Ansible playbooks are expected to take over.
You will find some basic configuration playbooks in the ansible dir which you can use to add layers to your fresh machine's security. They are, however, just like any security measure, not enough on their own and cannot be exhaustive due to the simple fact that SEC is based on your own threat model and not mine.
If you are using the tool on already properly secured infrastructure, I have nothing to tell you except have fun with it if you don't already have a better way to do what Quickstrap does. I have built the scripts keeping in mind that they may be used on machines that have already been set up. For this reason, I have tried to not take any liberties that may break your setup.
FAQs
I already have an ansible user and playbooks, how can I use them with quickstrap --run-ansible?
-
Copy your existing Ansible user's private ssh key file to the dir-quickstrap/keys directory (and apply chmod 600 to it).
-
Get your hosts.ini file and either place it as the main hosts.ini file or in alt_hostfiles under the ansible/ dir.
-
If you are going to use the same Ansible user you already have for all new hosts you do not really need to do anything else beside make sure this user and its keyfile are set under group_vars/all.yml
-
Otherwise create a group for your hosts in the hosts file you provided (for example
[my_hosts]) and put your hosts under it. Then create group_vars/my_hosts.yml and place there the username of your pre-existing Ansible user as well as the keyfile location ( which is now essentially ../dir-quickstrap/keys/thekeyyouhave ). -
Any of your own playbooks you place under playbooks will be shown in --run-ansible. Make sure that all relevant variables you have are placed in either all.yml or my_hosts.yml or whatever other vars dir or files you may have (keep them all in the ansible dir).
License
SintesLab - Quickstrap Copyright (C) 2026 SintesLab (Kiril Shahamov)
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License, version 3 or (at your
option) any later version, as published by the Free Software Foundation.
It comes with ABSOLUTELY NO WARRANTY. See
LICENSES/GPL-3.0-or-later.txt for the
full text.
Third-party attribution
Some configuration structure in this project is adapted from
grafana/alloy-scenarios,
licensed under the Apache License, Version 2.0. See
LICENSES/Apache-2.0.txt for the full text.
The spinner, checkwinsize, and progress-bar scripts in
dir-quickstrap/payload-src/ are adapted from the amazing
Dave Eddy's
ysap projects
(progress-bar,
spinner),
licensed under the MIT License. See
LICENSES/MIT.txt for the full text.
AI Usage Disclosure
Portions of this project were developed with AI assistance (Claude, Anthropic),
in the spirit of the Linux kernel's Assisted-by disclosure convention: AI is
treated as a tool, not an author. All AI-assisted output was reviewed, tested,
and understood by a human before being included, and the author (Kiril
Shahamov) takes full responsibility for the correctness, security (or lack thereof), and
licensing of the final code.