feat(bundle): initial commit
Some checks failed
Build Bootstrap Bundle / build (push) Failing after 54s

This commit is contained in:
Sebastian Rust
2026-02-05 18:19:37 +01:00
commit 702b7d2fe1
8 changed files with 434 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
name: Build Bootstrap Bundle
on:
push:
branches: [main]
paths:
- 'bootstrap.yml'
- 'roles/**'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install ansible ansible-bundler
- name: Verify playbook syntax
run: |
ansible-playbook --syntax-check bootstrap.yml -e 'user_pubkey="test"'
- name: Build bundle
run: |
ansible-bundler bootstrap.yml -o bootstrap.run
- name: Generate checksums
run: |
sha256sum bootstrap.run > bootstrap.run.sha256
md5sum bootstrap.run > bootstrap.run.md5
- name: Upload to release storage
run: |
# Option 1: Upload to Gitea generic packages
curl -X PUT \
-H "Authorization: token ${{ secrets.DEPLOY_TOKEN }}" \
-T bootstrap.run \
"${{ github.server_url }}/api/packages/${{ github.repository_owner }}/generic/bootstrap/latest/bootstrap.run"
curl -X PUT \
-H "Authorization: token ${{ secrets.DEPLOY_TOKEN }}" \
-T bootstrap.run.sha256 \
"${{ github.server_url }}/api/packages/${{ github.repository_owner }}/generic/bootstrap/latest/bootstrap.run.sha256"
# Alternative: Upload as artifact (for manual download)
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: bootstrap-bundle
path: |
bootstrap.run
bootstrap.run.sha256
retention-days: 90

34
.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# Secrets - NEVER commit
*.secret.yml
*.secret.yaml
secrets/
vault.yml
# Inventory files (contain hostnames/IPs)
inventory
inventory.txt
inventory.*.txt
# Pre-built bundles (CI builds fresh)
*.run
# Ansible artifacts
*.retry
.ansible/
# Python
__pycache__/
*.pyc
.venv/
venv/
# Editor
*.swp
*.swo
*~
.idea/
.vscode/
# OS
.DS_Store
Thumbs.db

77
README.md Normal file
View File

@@ -0,0 +1,77 @@
# Host Bootstrap Bundle
Ansible playbook for bootstrapping new servers with a secure user and hardened SSH configuration. Designed to be built with [ansible-bundler](https://pypi.org/project/ansible-bundler/) and distributed via CI/CD.
## Features
- Creates a user with SSH key authentication
- Configures passwordless sudo (optional)
- Hardens SSH with secure defaults (no root login, no password auth)
- Works with Debian, Ubuntu, RHEL/CentOS, Arch Linux
## Usage
### Direct with Ansible
```bash
ansible-playbook bootstrap.yml -i "host," -e user_name=operator \
-e 'user_pubkey="ssh-ed25519 AAAA..."'
```
### With Bundled Version
```bash
# Download and verify
curl -sL https://your-server/bootstrap.run -o /tmp/bootstrap.run
curl -sL https://your-server/bootstrap.run.sha256 -o /tmp/bootstrap.run.sha256
cd /tmp && sha256sum -c bootstrap.run.sha256
# Run
chmod +x /tmp/bootstrap.run
./bootstrap.run -e user_name=operator \
-e 'user_pubkey="ssh-ed25519 AAAA..."' \
-e user_password=changeme
```
## Variables
### Required
| Variable | Description |
|----------|-------------|
| `user_name` | Username to create |
| `user_pubkey` | SSH public key (or use `user_pubkey_file` / `user_pubkey_url`) |
### Optional
| Variable | Default | Description |
|----------|---------|-------------|
| `user_password` | - | Password for the user |
| `user_shell` | `/bin/bash` | User's login shell |
| `user_home` | `/home/{user}` | Home directory |
| `user_sudo_enabled` | `true` | Enable sudo access |
| `user_sudo_nopasswd` | `true` | Passwordless sudo |
| `user_pubkey_exclusive` | `true` | Replace existing authorized_keys |
| `ssh_server_ports` | `["22"]` | SSH port(s) |
| `ssh_permit_root_login` | `"no"` | Allow root SSH login |
| `ssh_allow_users` | - | Restrict SSH to specific users |
See `bootstrap.example.yml` for all options.
## Building
```bash
pip install ansible ansible-bundler
ansible-bundler bootstrap.yml -o bootstrap.run
sha256sum bootstrap.run > bootstrap.run.sha256
```
## CI/CD
The included Gitea Actions workflow (`.gitea/workflows/build.yml`) automatically builds and publishes the bundle on push to main.
Required secret: `DEPLOY_TOKEN` - Gitea token with `write:package` scope.
## License
MIT

46
bootstrap.example.yml Normal file
View File

@@ -0,0 +1,46 @@
---
# Example configuration for bootstrap.yml
# Copy this to bootstrap.vars.yml and customize
# Required: User configuration
user_name: "operator"
# SSH public key (choose one method)
# Method 1: Direct key
user_pubkey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... your-key-comment"
# Method 2: From local file
# user_pubkey_file: "~/.ssh/id_ed25519.pub"
# Method 3: From URL (e.g., GitHub keys)
# user_pubkey_url: "https://github.com/username.keys"
# Optional: User password (for console access or sudo with password)
# user_password: "changeme"
# Optional: User configuration
# user_home: "/home/operator"
# user_uid: 1000
# user_shell: /bin/bash
# user_groups:
# - wheel
# - docker
# Sudo configuration
user_sudo_enabled: true
user_sudo_nopasswd: true
# Replace all existing authorized_keys with just this key
user_pubkey_exclusive: true
# SSH hardening options (defaults are secure)
# ssh_server_ports: ["22"]
# ssh_permit_root_login: "no"
# ssh_server_password_login: false
# ssh_max_auth_retries: 2
# Restrict SSH access to specific users (recommended after setup)
# ssh_allow_users: "operator"
# Allow TCP forwarding if needed (for SSH tunnels)
# ssh_allow_tcp_forwarding: "local"

88
bootstrap.yml Normal file
View File

@@ -0,0 +1,88 @@
---
# Generic Host Bootstrap Playbook
#
# This playbook sets up a user and hardens SSH on target hosts.
# Designed to be built with ansible-bundler.
#
# Usage:
# ansible-playbook bootstrap.yml -i inventory -e @vars.yml
#
# Or with bundled version:
# ./bootstrap.run -e user_name=operator -e user_pubkey="ssh-ed25519 AAAA..."
#
# Required variables:
# - user_name: Username to create
# - One of: user_pubkey, user_pubkey_file, user_pubkey_url
#
# Optional variables (see roles/users/defaults/main.yml for full list):
# - user_password: Password for the user
# - user_sudo_enabled: Enable sudo (default: true)
# - user_sudo_nopasswd: Passwordless sudo (default: true)
# - ssh_server_ports: SSH port(s) (default: ["22"])
# - ssh_permit_root_login: Allow root login (default: "no")
#
- name: Bootstrap and harden host
hosts: all
gather_facts: yes
vars:
# User defaults (override via -e or vars file)
user_name: "operator"
# SSH hardening defaults - secure by default
ssh_permit_root_login: "no"
ssh_server_password_login: false
ssh_client_password_login: false
ssh_allow_tcp_forwarding: "no"
ssh_allow_agent_forwarding: false
ssh_x11_forwarding: false
ssh_permit_tunnel: "no"
ssh_use_pam: true
ssh_print_motd: false
ssh_print_last_log: false
ssh_max_auth_retries: 2
ssh_client_alive_interval: 300
ssh_client_alive_count: 3
# Include sshd_config.d for distro-specific configs
sshd_custom_options:
- "Include /etc/ssh/sshd_config.d/*"
# Restrict SSH to created user (set to empty string to allow all users)
# ssh_allow_users: "{{ user_name }}"
pre_tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: yes
cache_valid_time: 3600
become: yes
when: ansible_os_family == "Debian"
- name: Ensure sudo is installed
ansible.builtin.package:
name: sudo
state: present
become: yes
roles:
- role: users
become: yes
- role: ssh_hardening
become: yes
post_tasks:
- name: Display connection info
ansible.builtin.debug:
msg: |
Host bootstrap complete!
User '{{ user_name }}' has been created with SSH key authentication.
SSH has been hardened with the following settings:
- Root login: {{ ssh_permit_root_login }}
- Password authentication: {{ ssh_server_password_login }}
- Port(s): {{ ssh_server_ports | default(['22']) | join(', ') }}
To connect: ssh {{ user_name }}@{{ ansible_host | default(inventory_hostname) }}

View File

@@ -0,0 +1,37 @@
---
# User configuration defaults
# Username to create (required - must be overridden)
# user_name: "operator"
# User shell
user_shell: /bin/bash
# Home directory (defaults to /home/{{ user_name }} if not set)
# user_home: "/home/operator"
# User UID (optional - let system assign if not set)
# user_uid: 1000
# Additional groups for the user (optional)
# user_groups:
# - wheel
# - docker
# Append to existing groups instead of replacing
user_groups_append: true
# User password (optional - if not set, password login disabled)
# user_password: "changeme"
# Sudo configuration
user_sudo_enabled: true
user_sudo_nopasswd: true
# SSH public key (exactly one of these must be defined)
# user_pubkey: "ssh-ed25519 AAAA..."
# user_pubkey_file: "/path/to/key.pub"
# user_pubkey_url: "https://github.com/username.keys"
# Replace all existing authorized keys with just this one
user_pubkey_exclusive: true

25
roles/users/meta/main.yml Normal file
View File

@@ -0,0 +1,25 @@
---
galaxy_info:
author: "Jack"
description: "Creates and configures a user with SSH key authentication and optional sudo access"
license: "MIT"
min_ansible_version: "2.9"
platforms:
- name: Archlinux
versions:
- all
- name: Debian
versions:
- bullseye
- bookworm
- name: Ubuntu
versions:
- focal
- jammy
- noble
- name: EL
versions:
- "8"
- "9"
dependencies: []

View File

@@ -0,0 +1,66 @@
---
- name: Validate pubkey configuration
ansible.builtin.fail:
msg: "Only one of pubkey, pubkey_file, or pubkey_url can be defined"
when: >
(user_pubkey is defined and (user_pubkey_file is defined or user_pubkey_url is defined)) or
(user_pubkey_file is defined and user_pubkey_url is defined)
- name: Validate at least one pubkey source is defined
ansible.builtin.fail:
msg: "At least one of pubkey, pubkey_file, or pubkey_url must be defined"
when:
- user_pubkey is not defined
- user_pubkey_file is not defined
- user_pubkey_url is not defined
- name: Create user account
ansible.builtin.user:
name: "{{ user_name }}"
state: present
shell: "{{ user_shell }}"
createhome: yes
home: "{{ user_home | default('/home/' + user_name) }}"
uid: "{{ user_uid | default(omit) }}"
groups: "{{ user_groups | default(omit) }}"
append: "{{ user_groups_append | default(true) }}"
password: "{{ user_password | password_hash('sha512') if user_password is defined else omit }}"
update_password: "{{ 'always' if user_password is defined else 'on_create' }}"
become: yes
- name: Configure sudoers for user
ansible.builtin.lineinfile:
dest: /etc/sudoers.d/{{ user_name }}
line: "{{ user_name }} ALL=(ALL) {{ 'NOPASSWD: ' if user_sudo_nopasswd else '' }}ALL"
create: yes
mode: "0440"
validate: 'visudo -cf %s'
become: yes
when: user_sudo_enabled | bool
- name: Add SSH public key (direct)
ansible.builtin.authorized_key:
user: "{{ user_name }}"
key: "{{ user_pubkey }}"
state: present
exclusive: "{{ user_pubkey_exclusive | bool }}"
become: yes
when: user_pubkey is defined
- name: Add SSH public key (from file)
ansible.builtin.authorized_key:
user: "{{ user_name }}"
key: "{{ lookup('file', user_pubkey_file) }}"
state: present
exclusive: "{{ user_pubkey_exclusive | bool }}"
become: yes
when: user_pubkey_file is defined
- name: Add SSH public key (from URL)
ansible.builtin.authorized_key:
user: "{{ user_name }}"
key: "{{ lookup('url', user_pubkey_url) }}"
state: present
exclusive: "{{ user_pubkey_exclusive | bool }}"
become: yes
when: user_pubkey_url is defined