# ansible-linux-baseline

An opinionated, incrementally-adoptable **security baseline for fresh
Debian/Ubuntu servers**, packaged as an Ansible role.

Point it at a box that just finished its OS install and it will bring the host
to a known-good starting state: admin users with SSH keys, a hardened SSH
daemon, sensible network/kernel `sysctl` settings, automatic security updates,
and a persistent, size-capped journal.

## The problem it solves

Every new server starts the same way — password SSH is on, `root` can log in,
there are no non-root admins, nothing patches itself, and logs vanish on
reboot. Fixing that by hand is easy to get *almost* right and easy to do
slightly differently each time. That drift is exactly what bites you six
months later when one host behaves unlike the rest.

This role turns "the way we set up a server" into code: one reviewable,
idempotent definition you can run against one box or a hundred, re-run safely,
and read instead of remember. If I do something twice, it gets a role — this is
that principle applied to host bring-up.

## What it configures

| Area | What it does | Off switch |
|------|--------------|-----------|
| **Packages** | Installs a small baseline (`chrony`, `curl`, `htop`, …); can purge unwanted ones | `linux_baseline_manage_packages` |
| **Admin users** | Creates users, adds them to a sudo group, installs their SSH keys | `linux_baseline_manage_users` |
| **SSH** | Drop-in hardening: no root login, no passwords, key-only, login limits | `linux_baseline_manage_ssh` |
| **sysctl** | Network/kernel hardening (`rp_filter`, `tcp_syncookies`, ASLR, …) | `linux_baseline_manage_sysctl` |
| **Updates** | `unattended-upgrades` scoped to the security pocket, optional reboot window | `linux_baseline_manage_updates` |
| **journald** | Persistent storage with a size cap and retention limit | `linux_baseline_manage_journald` |

Each area is a separate task file behind its own toggle, so you can adopt the
role one piece at a time on hosts you don't fully control yet.

## Design choices worth knowing

- **Won't lock you out.** SSH password authentication is disabled by default —
  but the role *refuses to apply that* unless at least one admin user has an
  authorized key, and users are created before SSH is hardened. The safe path
  is the default path.
- **Drop-ins, not edits.** SSH, sysctl, journald, and unattended-upgrades are
  configured with files in `*.conf.d/` directories rather than rewriting the
  distro's own config. Upgrades stay clean and the role's footprint is obvious.
- **Validated before applied.** The sshd config is checked with `sshd -t` and
  sudoers with `visudo -cf` *before* being put in place, so a bad template can
  never leave you with a broken daemon.
- **Debian-only, on purpose.** A first task asserts `os_family == Debian`.
  Narrow and tested beats broad and untested.

## Requirements

- Ansible **2.15+** (`ansible-core`)
- The `ansible.posix` collection (see `requirements.yml`)
- Target hosts: Debian 12 (bookworm) or Ubuntu 22.04 / 24.04

```bash
ansible-galaxy collection install -r requirements.yml
```

## Usage

Minimal playbook — the one thing you *must* supply is at least one admin user
with a public key:

```yaml
- name: Apply the security baseline
  hosts: servers
  become: true
  vars:
    linux_baseline_admin_users:
      - name: alice
        authorized_keys:
          - "ssh-ed25519 AAAA... alice@laptop"
  roles:
    - role: ansible-linux-baseline
```

```bash
ansible-playbook -i inventory.ini examples/playbook.yml
```

A fuller example lives in [`examples/playbook.yml`](examples/playbook.yml).

## Configuration

All variables and their defaults are documented inline in
[`defaults/main.yml`](defaults/main.yml). The ones you'll reach for first:

| Variable | Default | Purpose |
|----------|---------|---------|
| `linux_baseline_admin_users` | `[]` | Admin users to create, each with `authorized_keys` |
| `linux_baseline_admin_group` | `admins` | Sudo group the admins are added to |
| `linux_baseline_admin_nopasswd` | `false` | Passwordless sudo for the admin group |
| `linux_baseline_ssh_password_authentication` | `false` | Leave off unless you have a reason |
| `linux_baseline_ssh_permit_root_login` | `"no"` | Root SSH login |
| `linux_baseline_sysctl` | see defaults | Map of sysctl keys → values |
| `linux_baseline_unattended_automatic_reboot` | `false` | Auto-reboot after security updates |
| `linux_baseline_journald_max_use` | `500M` | Cap on persistent journal size |

Put real users, keys, and any secrets in your own inventory or a
`vault`-encrypted vars file. Nothing sensitive belongs in this repo.

## Testing

The role is tested with [Molecule](https://ansible.readthedocs.io/projects/molecule/)
against a Docker container. `converge.yml` applies the role, and `verify.yml`
asserts the outcomes — hardened `sshd -T` output, the admin user in its group,
live sysctls, `unattended-upgrades` present, persistent journald.

```bash
pip install "molecule-plugins[docker]" molecule ansible-core
ansible-galaxy collection install -r requirements.yml
molecule test
```

CI (`.github/workflows/ci.yml`) runs `yamllint`, `ansible-lint`, and the full
Molecule suite on every push and pull request.

## License

MIT — see [LICENSE](LICENSE).
