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