Skip to content

Instantly share code, notes, and snippets.

@gkedge
Last active January 27, 2023 18:29
Show Gist options
  • Save gkedge/0a16c48a867d86c033077e62bbbabbca to your computer and use it in GitHub Desktop.
Save gkedge/0a16c48a867d86c033077e62bbbabbca to your computer and use it in GitHub Desktop.
Ansible Playbook: Hardened SSL and Vagrant Ready (ubuntu 20.04)
---
# Initial server setup; run on guest after 'Assumptions' are addressed:
# Forked from: https://www.vultr.com/docs/how-to-configure-a-new-ubuntu-server-with-ansible
#
# VirtualBox OS Prep
#
# Assumptions:
# Already have used VirtualBox Manager to:
# * create new VirtualBox ubuntu 20.04.3+ using VirtualBox Manager:
# * General > Basic:
# * Name: "vagrant-hardened-ubuntu-20.04.5"; Type: Linux; Version: Ubuntu (64-bit)
# * set System settings as desired
# * on Storage tab, assign ubuntu-20.04.3-desktop-amd64.iso to Optical Drive / select Live CD/DVD
# * on Network tab,
# * enable Adapter 1 to NAT
# * Port Forward SSH Host Port 2222 | Guest Port 22 (for vagrant) or 2225 (for a non-vagrant provisioner?)
# * started new VM and logged in as user added during the creation of the new VM steps above.
# * This user will already have admin privileges and sudo powers.
# * removed any previous ansible:
# $ su root
# Password <password-of-logged-in-user>
# * add user to sudo group
# $ usermod -aG sudo username
# * add user to sudo file
# $ visudo
# * add user as no password/all access
# <username> ALL=(ALL) NOPASSWD:ALL
# * save/exit visudo exit root terminal
# $ sudo apt update -y
# $ sudo apt upgrade -y
# $ sudo apt remove -y ansible
# * add ansible PPA repo:
# $ sudo add-apt-repository --yes --update ppa:ansible/ansible
# $ sudo apt update -y
# For VirtualBox VM:
# Test to see if right packages are installed:
# sudo dpkg -l | grep -E "dkms|linux-headers-$(uname -r)|build-essential"
# If not:
# $ sudo apt install software-properties-common build-essential dkms linux-headers-$(uname -r)
# For VirtualBox VM and real HW machine
# $ sudo apt install -y software-properties-common python3-pip python3.9 ansible
# For VirtualBox VM
# * shutdown (not restart)
# $ sudo shutdown now
# Using VirtualBox Manager:
# * on Storage tab, assign VBoxGuestAdditions.iso to Optical Drive / DESELECT Live CD/DVD
# * start new VM and login as user added during the creation of the new VM steps above.
# Click CD and install Guest Tools, then shutdown when done.
# $ sudo shutdown now
# Take VirtualBox Snapshot named "Before Hardening"
# For real HW machine
# * restart
# $ sudo shutdown -r now
# Instructions:
# For VirtualBox VM:
# Using VirtualBox Manager, start VM.
#
# For VirtualBox VM and real HW machine
# Login as user added during the creation of VM or install of ubuntu 20.04+ on a real HW machine.
# Create an ansible vault (passwd.yml) to contain:
# * worthy root password to replace existing root password
# * provision_user ('vagrant' as a user name for VirtualBox VM's managed by vagrant(1) or
# 'provider' for ubuntu release on bare metal).
# * provision_password
# * provision_public_key (only necessary if different than vagrant insecure public key)
# $ cd .ssh
# $ ansible-vault create passwd.yml
# Vault password: <remember-this>
#
# root_passwd: <worthy-root-password>
# provision_user: <provision user that is not the metsci_username>
# provision_password: <provision user passwd>
# provision_public_key: ssh-rsa AAAAB3NzaC1y....
# metsci_username: kedge
# metsci_password: <your current Metron user password>
# git_public_ssh_key: ssh-rsa AAAAB3NzaC1yc2EAAA...
# git_private_ssh_key: |
# -----BEGIN OPENSSH PRIVATE KEY-----
# ...
#
# Download this script (still on guest as initial user)
# $ wget --no-check-certificate https://gist.githubusercontent.com/gkedge/0a16c48a867d86c033077e62bbbabbca/raw/vagrant-hardened-ubuntu-20-04.yml
# $ sudo ansible-playbook --ask-vault-pass --extra-vars '@passwd.yml' vagrant-hardened-ubuntu-20-04.yml
# Vault password: <enter remember-this>
#
# After finished:
# * restart
# $ sudo shutdown -r now
# * Test from host; note <host-user> !!
# * For vagrant (using GitBash):
# $ ssh vagrant@127.0.0.1 -p 2222 -i /c/Users/<host-user>/.vagrant.d/insecure_private_key
# * For a different provision_user:
# $ ssh <provision_user>@127.0.0.1 -p 2222 -i /c/Users/<host-user>/.ssh/<provision_private_key>
#
# The VM is ready to run Ansible playbooks to further provision. If you are going to use
# vagrant to assist with that further provisioning, turn this VirtualBox VM into a
# VagrantBox:
# * log into the VM as the provision_user,
# * zero out any freespace to allow for the Vagrant Box to maximally compressed:
# $ sudo dd if=/dev/zero of=/EMPTY bs=1M
# [sudo] password for vagrant:
# dd: error writing '/EMPTY': No space left on device
# 20820+0 records in
# 20819+0 records out
# 21830807552 bytes (22 GB, 20 GiB) copied, 95.9642 s, 227 MB/s
# The error is expected as all the freespace was mounted onto /EMPTY and zeroed out. Free up by
# deleting /EMPTY content:
# $ sudo rm -f /EMPTY
# $ sudo shutdown now
#
# On Host (using GitBash):
# $ mkdir -p ~/VagrantBoxVMs
# $ cd ~/VagrantBoxVMs
# $ vagrant package --base vagrant-hardened-ubuntu-20.04.5 --output vagrant-hardened-ubuntu-20.04.5.box
# The compressed box should a touch over 4G.
# Install base vagrant-hardened-ubuntu2004.box into Vagrant using a UMI reference
# (stores in ~/.vagrant.d/boxes).
# $ vagrant box add --name vagrant-hardened-ubuntu-20.04.5 "file:///C:/Users/<userid>/VagrantBoxVMs/vagrant-hardened-ubuntu-20.04.5.box"
#
# You are now essentially taking up 2x of that box's 4G of space taken up for the same thing:
# 1) the ~/VagrantBoxVMs/vagrant-hardened-ubuntu-20.04.5.box
# 2) added to ~/.vagrant.d
# Best to back up ~/VagrantBoxVMs/vagrant-hardened-ubuntu2004.box and delete it from your machine.
#
# I leave it to the vagrantites among you to set up further Ansible provisioning using that tool.
- hosts: localhost
become: yes
force_handlers: True
vars:
ssh_port: "{{ 22 if provision_user == 'vagrant' else 2255 }}"
tmzone: America/New_York
sudo_timeout: 20
f2b_jail_local: |
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1
findtime = 1h
bantime = 2h
maxretry = 3
[sshd]
enabled = true
port = {{ ssh_port }}
# Using a URL to the ansible.posix.authorized_key's key seems problematic where copying the
# 'current' public insecure key from the URL:
# "https://raw.githubusercontent.com/hashicorp/vagrant/master/keys/vagrant.pub"
# ... seems to work. If this hard-coded value changes at that URL, this needs
# to change to reflect the new value.
vagrant_insecure_public_key: "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key"
tasks:
- name: Get datestamp from the system
ansible.builtin.shell: date +"%Y%m%d"
register: dstamp
- name: Set current date stamp variable
ansible.builtin.set_fact:
cur_date: "{{ dstamp.stdout }}"
# Update and install the base software
- name: Update apt package cache
ansible.builtin.apt:
update_cache: yes
cache_valid_time: 3600
- name: add ansible/ansible
ansible.builtin.apt_repository:
repo: "{{ item }}"
state: present
with_items:
- ppa:ansible/ansible
- name: Upgrade installed APT packages
ansible.builtin.apt:
upgrade: dist
register: upgrade
retries: 15
delay: 5
until: upgrade is success
tags: [ harden ]
- name: Ensure resolveconf replaces Network Manager
ansible.builtin.apt:
name:
- needrestart
- resolvconf
state: latest
when: provision_user == 'vagrant'
tags: [ harden, skip_ansible_lint ]
# OR
# - name: Load VirtualBox Guest Additions dedicated to an ubuntu release
# ansible.builtin.apt:
# name:
# - virtualbox-guest-additions-iso
# when: provision_user == 'vagrant'
- name: Ensure hardening tools are installed
ansible.builtin.apt:
name:
- fail2ban
- unbound
state: latest
when: provision_user != 'vagrant'
tags: [ harden, skip_ansible_lint ]
- name: Force security to always update to latest
ansible.builtin.apt:
name:
- openssh-server
state: latest
tags:
- always
- skip_ansible_lint
- name: Ensure that these software packages are installed to assist debug
ansible.builtin.apt:
name:
- cifs-utils
- unzip
- net-tools
- nmap
- htop
- terminator
- xclip
- gnome-nettool
state: present
tags:
- periodic
- name: Detect /etc/sudoers
ansible.builtin.stat: path=/etc/sudoers
register: etc_sudoers
# 'requiretty' in /etc/sudoers can hose ssh pipelining. See:
# https://docs.ansible.com/ansible/2.4/intro_configuration.html#pipelining
- name: disable requiretty in sudo, so that so Ansible ssh pipelining will work
ansible.builtin.lineinfile:
dest: /etc/sudoers
regexp: '^(Defaults\s+requiretty)$'
line: '# \1'
backrefs: yes
backup: yes
validate: '/usr/sbin/visudo -cf %s'
when: etc_sudoers.stat.exists | bool
# Set sudo password timeout (default is 15 minutes)
- name: Set sudo password timeout.
ansible.builtin.lineinfile:
path: /etc/sudoers
state: present
backup: yes
regexp: '^Defaults\tenv_reset'
line: 'Defaults env_reset, timestamp_timeout={{ sudo_timeout }}'
validate: '/usr/sbin/visudo -cf %s'
when: provision_user != 'vagrant' and etc_sudoers.stat.exists | bool
tags: [ harden ]
- name: Create/update provision user with sudo privileges
ansible.builtin.user:
name: "{{ provision_user }}"
password: "{{ provision_password | password_hash('sha512') }}"
state: present
groups: sudo
append: true
shell: /bin/bash
- name: "Add user {{ provision_user }} to sudo"
ansible.builtin.lineinfile:
path: '/etc/sudoers.d/{{ provision_user }}'
line: '{{ provision_user }} ALL=(ALL) NOPASSWD: ALL'
state: present
mode: 0440
create: yes
backup: yes
validate: 'visudo -cf %s'
when: etc_sudoers.stat.exists | bool
- name: "chmod user {{ provision_user }}'s home dir"
ansible.builtin.file:
path: "/home/{{ provision_user }}"
mode: "0700"
- name: "Ensure authorized key for remote user is installed for {{ provision_user }}"
ansible.posix.authorized_key:
user: "{{ provision_user }}"
state: present
key: "{{ provision_public_key }}"
when: provision_user != 'vagrant' and provision_public_key is defined
# Accessing the vagrant insecure public key directly from GitHub
# apparently no longer plays nice.
# wget --no-check-certificate \
# https://raw.github.com/mitchellh/vagrant/master/keys/vagrant.pub \
# -O /home/vagrant/.ssh/authorized_keys
- name: "Ensure vagrant-insecure key for remote user is installed for {{ provision_user }}"
ansible.posix.authorized_key:
user: "{{ provision_user }}"
state: present
key: "{{ vagrant_insecure_public_key }}"
when: provision_user == 'vagrant'
- name: Ensure authorized key for root user is installed
ansible.posix.authorized_key:
user: root
state: present
key: "{{ provision_public_key }}"
when: provision_user == 'vagrant' and provision_public_key is defined
- name: Update root user password.
ansible.builtin.user:
name: root
password: "{{ root_passwd | password_hash('sha512') }}"
- name: Update SSH configuration to be more secure.
ansible.builtin.lineinfile:
dest: /etc/ssh/sshd_config
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
backup: yes
state: present
validate: 'sshd -t -f %s'
with_items:
- regexp: "^(#)?PasswordAuthentication"
line: "PasswordAuthentication yes"
- regexp: "^(#)?PermitRootLogin"
line: "PermitRootLogin yes"
when: provision_user == 'vagrant'
notify: Restart sshd
- name: Update SSH configuration to be more secure.
ansible.builtin.lineinfile:
dest: /etc/ssh/sshd_config
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
state: present
validate: 'sshd -t -f %s'
backup: yes
with_items:
- regexp: "^(#)?GSSAPIAuthentication"
line: "#GSSAPIAuthentication yes"
- regexp: "^(#)?GSSAPICleanupCredentials"
line: "#GSSAPICleanupCredentials no "
- regexp: "^(#)?PasswordAuthentication"
line: "PasswordAuthentication no"
- regexp: "^(#)?PermitRootLogin"
line: "PermitRootLogin prohibit-password"
- regexp: "^(#)?UseDNS"
line: "UseDNS no "
- regexp: "^(#)?ChallengeResponseAuthentication"
line: "ChallengeResponseAuthentication no"
- regexp: "^(#)?AuthorizedKeysFile"
line: "AuthorizedKeysFile %h/.ssh/authorized_keys"
- regexp: "^(#)?Subsystem\\s+sftp\\s+/usr/lib/openssh/sftp-server"
line: "Subsystem sftp /usr/lib/openssh/sftp-server -f AUTHPRIV -l INFO"
- regexp: "^(#)?Port"
line: "Port {{ ssh_port }}"
when: provision_user != 'vagrant'
notify: Restart sshd
- name: Update SSH configuration to be more secure.
ansible.builtin.lineinfile:
dest: /etc/ssh/sshd_config
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
backup: yes
state: present
validate: 'sshd -t -f %s'
with_items:
- regexp: "^(#)?PasswordAuthentication"
line: "PasswordAuthentication yes"
- regexp: "^(#)?PermitRootLogin"
line: "PermitRootLogin yes"
when: provision_user == 'vagrant'
notify: Restart sshd
- name: Set user PS1 to a two-line prompt
ansible.builtin.lineinfile:
dest: "/home/{{ provision_user }}/.bashrc"
insertafter: EOF
line: "PS1='${debian_chroot:+($debian_chroot)}\\[\\033[01;32m\\]\\u@\\h\\[\\033[00m\\]:\\[\\033[01;34m\\]\\w\\[\\033[00m\\]\\n\\$ '"
state: present
- name: Set root PS1 to a two-line prompt
ansible.builtin.lineinfile:
path: '/root/.bashrc'
state: present
insertafter: EOF
line: PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\n\$ '
# Configure a firewall
- name: Disable and reset ufw firewall to installation defaults.
ansible.builtin.ufw:
state: reset
when: provision_user != 'vagrant'
tags: [ harden ]
- name: Find backup rules to delete
ansible.builtin.find:
paths: /etc/ufw
patterns: "*.{{ cur_date }}_*"
use_regex: no
when: provision_user != 'vagrant'
register: files_to_delete
tags: [ harden ]
- name: Delete ufw backup rules
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
with_items: "{{ files_to_delete.files }}"
when: provision_user != 'vagrant'
tags: [ harden ]
- name: Allow ssh port '{{ ssh_port }}'.
ansible.builtin.ufw:
rule: allow
proto: tcp
port: '{{ ssh_port }}'
state: enabled
when: provision_user != 'vagrant'
tags: [ harden ]
- name: Turn UFW logging off
ansible.builtin.ufw:
logging: "off"
when: provision_user != 'vagrant'
tags: [ harden ]
- name: configure fail2ban for ssh
ansible.builtin.copy:
dest: /etc/fail2ban/jail.local
content: "{{ f2b_jail_local }}"
owner: root
group: root
mode: 0644
when: provision_user != 'vagrant'
notify:
- Restart fail2ban
tags: [ harden ]
# simple shell script to display fail2ban-client status info; usage:
# f2bst
# f2bst sshd
- name: Configure f2bst
ansible.builtin.copy:
dest: /usr/local/bin/f2bst
content: |
#!/usr/bin/sh
fail2ban-client status $*
owner: root
group: root
mode: 0750
when: provision_user != 'vagrant'
tags: [ harden ]
- name: run needrestart
ansible.builtin.command: needrestart -l -r a
when: not reboot_required.stat.exists | bool and upgrade.changed | bool
- name: Reboot the server if needed
ansible.builtin.reboot:
msg: "Reboot initiated by Ansible because of reboot required file."
connect_timeout: 5
reboot_timeout: 600
pre_reboot_delay: 0
post_reboot_delay: 30
test_command: whoami
when: reboot_required.stat.exists | bool and provision_user != 'vagrant'
- name: Remove old packages from the cache
ansible.builtin.apt:
autoclean: yes
- name: Remove dependencies that are no longer needed
ansible.builtin.apt:
autoremove: yes
purge: yes
handlers:
# Test with:
# $ sudo systemctl status sshd
# ssh.service - OpenBSD Secure Shell server
# Loaded: loaded (/lib/systemd/system/ssh.service; enabled; vendor preset: enabled)
# Active: active (running) since Sun 2022-02-13 15:41:45 EST; 22min ago
# Docs: man:sshd(8)
# man:sshd_config(5)
# Process: 10986 ExecStartPre=/usr/sbin/sshd -t (code=exited, status=0/SUCCESS)
# Main PID: 10987 (sshd)
# Tasks: 1 (limit: 19126)
# Memory: 1.0M
# CGroup: /system.slice/ssh.service
# └─10987 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
- name: Restart sshd
ansible.builtin.service:
name: sshd
state: restarted
when: not reboot_required.stat.exists | bool
- name: Restart fail2ban
ansible.builtin.service:
name: fail2ban
state: restarted
when: not reboot_required.stat.exists | bool
tags: [ harden ]
- name: restart systemd-resolved
ansible.builtin.service:
name: systemd-resolved
state: restarted
when: not reboot_required.stat.exists | bool
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment