feat(bundle): initial commit
Some checks failed
Build Bootstrap Bundle / build (push) Failing after 54s
Some checks failed
Build Bootstrap Bundle / build (push) Failing after 54s
This commit is contained in:
61
.gitea/workflows/build.yml
Normal file
61
.gitea/workflows/build.yml
Normal 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
34
.gitignore
vendored
Normal 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
77
README.md
Normal 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
46
bootstrap.example.yml
Normal 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
88
bootstrap.yml
Normal 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) }}
|
||||||
37
roles/users/defaults/main.yml
Normal file
37
roles/users/defaults/main.yml
Normal 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
25
roles/users/meta/main.yml
Normal 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: []
|
||||||
66
roles/users/tasks/main.yml
Normal file
66
roles/users/tasks/main.yml
Normal 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
|
||||||
Reference in New Issue
Block a user