Last active
January 27, 2023 18:29
-
-
Save gkedge/0a16c48a867d86c033077e62bbbabbca to your computer and use it in GitHub Desktop.
Ansible Playbook: Hardened SSL and Vagrant Ready (ubuntu 20.04)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- | |
# 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 [email protected] -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