├── .gitignore ├── roles ├── ssh │ ├── handlers │ │ └── main.yml │ └── tasks │ │ └── main.yml ├── fail2ban │ ├── handlers │ │ └── main.yml │ └── tasks │ │ └── main.yml ├── hostname │ ├── handlers │ │ └── main.yml │ └── tasks │ │ └── main.yml ├── newrelic │ ├── handlers │ │ └── main.yml │ └── tasks │ │ └── main.yml ├── timezone │ └── tasks │ │ └── main.yml ├── ufw │ └── tasks │ │ └── main.yml ├── essentials │ └── tasks │ │ └── main.yml ├── user │ └── tasks │ │ └── main.yml └── zsh │ └── tasks │ └── main.yml ├── src ├── apt │ └── 10periodic ├── zsh │ └── zshrc.j2 └── fail2ban │ └── jail.local ├── ansible.cfg ├── .editorconfig ├── hosts ├── initial_server_setup.yml ├── test_server_setup.yml ├── vars └── main.yml ├── Vagrantfile └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.vagrant 2 | *.log 3 | *.retry 4 | -------------------------------------------------------------------------------- /roles/ssh/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: restart ssh 3 | service: 4 | name: ssh 5 | state: restarted 6 | -------------------------------------------------------------------------------- /roles/fail2ban/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: restart fail2ban 3 | service: 4 | name: fail2ban 5 | state: restarted 6 | -------------------------------------------------------------------------------- /roles/hostname/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: restart hostname 3 | service: 4 | name: hostname 5 | state: restarted 6 | -------------------------------------------------------------------------------- /roles/newrelic/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: start newrelic 3 | service: 4 | name: newrelic-sysmond 5 | state: started 6 | -------------------------------------------------------------------------------- /src/apt/10periodic: -------------------------------------------------------------------------------- 1 | APT::Periodic::Update-Package-Lists "1"; 2 | APT::Periodic::Download-Upgradeable-Packages "1"; 3 | APT::Periodic::AutocleanInterval "7"; 4 | APT::Periodic::Unattended-Upgrade "1"; 5 | -------------------------------------------------------------------------------- /ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | inventory = ./hosts 3 | log_path = ./ansible.log 4 | # uncomment the below settings for use with Vagrant 5 | #private_key_file = ~/.vagrant.d/insecure_private_key 6 | #host_key_checking = False 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /hosts: -------------------------------------------------------------------------------- 1 | # Add hosts here, one per line. Additional groups can be created using 2 | # [group] syntax. Hosts can join multiple groups. 3 | 4 | [test] 5 | 192.168.2.2 6 | 7 | [production] 8 | # e.g 192.168.1.1 9 | # or host.example.com 10 | -------------------------------------------------------------------------------- /initial_server_setup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: production 3 | user: root 4 | 5 | vars_files: 6 | - vars/main.yml 7 | 8 | roles: 9 | - essentials 10 | - hostname 11 | - timezone 12 | - user 13 | - fail2ban 14 | - zsh 15 | - newrelic 16 | - ssh 17 | - ufw 18 | -------------------------------------------------------------------------------- /test_server_setup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: test 3 | user: vagrant 4 | become: true 5 | 6 | vars_files: 7 | - vars/main.yml 8 | 9 | roles: 10 | - essentials 11 | - hostname 12 | - timezone 13 | - user 14 | - fail2ban 15 | - zsh 16 | - newrelic 17 | - ssh 18 | - ufw 19 | -------------------------------------------------------------------------------- /roles/timezone/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Set the current timezone 3 | copy: 4 | content: "{{ timezone }}" 5 | dest: /etc/timezone 6 | owner: root 7 | group: root 8 | mode: 0644 9 | backup: yes 10 | register: timezone_changed 11 | 12 | - name: Update the timezone 13 | when: timezone_changed|succeeded 14 | command: dpkg-reconfigure --frontend noninteractive tzdata 15 | -------------------------------------------------------------------------------- /vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | user: luke 3 | password: $6$kgX6rY4/$EOL3Q50Yt8DbriTsqNZpWGVCSSibUVJIaS1BnCadVknfN8.1XXr1MCdkvEbnfS6x7xfYYSghbH7uS.JIXzIy./ 4 | public_key: ~/.ssh/id_rsa.pub 5 | 6 | fqdn: host.example.com 7 | hostname: host 8 | timezone: "Europe/London" 9 | ssh_port: 12345 10 | 11 | enable_newrelic: yes 12 | newrelic_license_key: XXXXXXXXXX 13 | enable_custom_shell: yes 14 | -------------------------------------------------------------------------------- /roles/fail2ban/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install fail2ban 3 | apt: 4 | name: fail2ban 5 | state: present 6 | update_cache: yes 7 | cache_valid_time: 3600 8 | 9 | - name: Copy custom fail2ban config 10 | copy: 11 | src: ./src/fail2ban/jail.local 12 | dest: /etc/fail2ban/jail.local 13 | owner: root 14 | group: root 15 | mode: 0644 16 | notify: restart fail2ban 17 | 18 | - name: Ensure fail2ban starts on a fresh reboot 19 | service: 20 | name: fail2ban 21 | state: started 22 | enabled: yes 23 | -------------------------------------------------------------------------------- /roles/hostname/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Set hostname 3 | hostname: 4 | name: "{{ fqdn }}" 5 | notify: restart hostname 6 | 7 | - name: Add correct IP address to hosts file 8 | lineinfile: 9 | dest: /etc/hosts 10 | insertafter: '^127\.0\.0\.1\slocalhost' 11 | line: "{{ ansible_default_ipv4.address }} {{ fqdn }} {{ hostname }}" 12 | state: present 13 | notify: restart hostname 14 | 15 | - name: Remove default line from hosts file 16 | lineinfile: 17 | dest: /etc/hosts 18 | regexp: '^127\.0\.1\.1' 19 | state: absent 20 | notify: restart hostname 21 | -------------------------------------------------------------------------------- /roles/ufw/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Allow ssh traffic on custom port 3 | ufw: 4 | rule: allow 5 | port: "{{ ssh_port }}" 6 | proto: tcp 7 | 8 | - name: Allow http traffic on port 80 9 | ufw: 10 | rule: allow 11 | port: 80 12 | proto: tcp 13 | 14 | - name: Allow https traffic on port 443 15 | ufw: 16 | rule: allow 17 | port: 443 18 | proto: tcp 19 | 20 | - name: Allow access to admin tools on port 22222 21 | ufw: 22 | rule: allow 23 | port: 22222 24 | proto: tcp 25 | 26 | - name: Enable firewall 27 | ufw: 28 | state: enabled 29 | policy: deny 30 | -------------------------------------------------------------------------------- /src/zsh/zshrc.j2: -------------------------------------------------------------------------------- 1 | # Path to your oh-my-zsh installation. 2 | export ZSH=/home/{{ user }}/.oh-my-zsh 3 | 4 | # Set name of theme to load 5 | ZSH_THEME="guru2" 6 | 7 | # Enable oh my zsh plugins 8 | plugins=(git) 9 | 10 | # Load oh my zsh 11 | source $ZSH/oh-my-zsh.sh 12 | 13 | # Load z 14 | source ~/z.sh 15 | 16 | # Configure PATH 17 | export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games" 18 | 19 | # Lines configured by zsh-newuser-install 20 | HISTFILE=~/.histfile 21 | HISTSIZE=1000 22 | SAVEHIST=1000 23 | setopt appendhistory autocd extendedglob 24 | bindkey -e 25 | -------------------------------------------------------------------------------- /roles/essentials/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Update all packages 3 | apt: 4 | update_cache: yes 5 | upgrade: safe 6 | autoremove: yes 7 | 8 | - name: Install essential packages 9 | apt: 10 | name: "{{ item }}" 11 | update_cache: yes 12 | cache_valid_time: 3600 13 | state: latest 14 | with_items: 15 | - unattended-upgrades 16 | - git 17 | - ntp 18 | 19 | - name: Enable unattended upgrades 20 | copy: 21 | src: ./src/apt/10periodic 22 | dest: /etc/apt/apt.conf.d/10periodic 23 | owner: root 24 | group: root 25 | mode: 0644 26 | 27 | - name: Ensure ntp starts on a fresh reboot 28 | service: 29 | name: ntp 30 | state: started 31 | enabled: yes 32 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # Defines our Vagrant environment 2 | # 3 | # -*- mode: ruby -*- 4 | # vi: set ft=ruby : 5 | 6 | Vagrant.configure("2") do |config| 7 | config.vm.box = "ubuntu/trusty64" 8 | 9 | config.vm.provider :virtualbox do |virtualbox| 10 | virtualbox.customize ["modifyvm", :id, "--memory", 1024] 11 | virtualbox.customize ["modifyvm", :id, "--cpus", 1] 12 | virtualbox.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] 13 | virtualbox.customize ["modifyvm", :id, "--ioapic", "on"] 14 | end 15 | 16 | config.vm.define "test" do |machine| 17 | machine.vm.hostname = "test" 18 | machine.vm.network :private_network, ip: "192.168.2.2" 19 | machine.ssh.insert_key = false 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /roles/user/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure sudo group is present 3 | group: 4 | name: sudo 5 | state: present 6 | 7 | - name: Ensure sudo group has sudo privileges 8 | lineinfile: 9 | dest: /etc/sudoers 10 | state: present 11 | regexp: "^%sudo" 12 | line: "%sudo ALL=(ALL:ALL) ALL" 13 | validate: "/usr/sbin/visudo -cf %s" 14 | 15 | - name: Add new super user 16 | user: 17 | name: "{{ user }}" 18 | password: "{{ password }}" 19 | groups: sudo 20 | append: yes 21 | state: present 22 | shell: /bin/bash 23 | update_password: always 24 | 25 | - name: Add SSH key for new super user 26 | authorized_key: 27 | user: "{{ user }}" 28 | key: "{{ lookup('file', item) }}" 29 | with_items: "{{ public_key }}" 30 | -------------------------------------------------------------------------------- /roles/newrelic/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install GPG key 3 | when: enable_newrelic 4 | apt_key: 5 | url: http://download.newrelic.com/548C16BF.gpg 6 | state: present 7 | register: gpg_key_installed 8 | 9 | - name: Add New Relic apt repository 10 | when: gpg_key_installed|succeeded 11 | copy: 12 | content: "deb http://apt.newrelic.com/debian/ newrelic non-free" 13 | dest: /etc/apt/sources.list.d/newrelic.list 14 | register: newrelic_repo_installed 15 | 16 | - name: Install New Relic system monitor 17 | when: newrelic_repo_installed|succeeded 18 | apt: 19 | name: newrelic-sysmond 20 | state: latest 21 | update_cache: yes 22 | register: newrelic_installed 23 | 24 | - name: Configure New Relic license key 25 | when: newrelic_installed|succeeded 26 | command: nrsysmond-config --set license_key={{ newrelic_license_key }} 27 | notify: start newrelic 28 | -------------------------------------------------------------------------------- /roles/ssh/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: ensure ssh is installed 3 | apt: 4 | pkg: openssh-server 5 | state: latest 6 | update_cache: true 7 | cache_valid_time: 3600 8 | notify: restart ssh 9 | 10 | - name: Change ssh port 11 | lineinfile: 12 | dest: /etc/ssh/sshd_config 13 | regexp: '^Port\s' 14 | line: "Port {{ ssh_port }}" 15 | state: present 16 | notify: restart ssh 17 | 18 | - name: Disable root login 19 | lineinfile: 20 | dest: /etc/ssh/sshd_config 21 | regexp: '^PermitRootLogin' 22 | line: "PermitRootLogin no" 23 | state: present 24 | notify: restart ssh 25 | 26 | - name: Disable password authentication 27 | lineinfile: 28 | dest: /etc/ssh/sshd_config 29 | regexp: '^PasswordAuthentication' 30 | line: "PasswordAuthentication no" 31 | state: present 32 | notify: restart ssh 33 | 34 | - name: Only allow SSH access for the newly created user 35 | lineinfile: 36 | dest: /etc/ssh/sshd_config 37 | insertafter: '^StrictModes' 38 | line: "AllowUsers {{ user }}" 39 | state: present 40 | notify: restart ssh 41 | -------------------------------------------------------------------------------- /roles/zsh/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install zsh 3 | when: enable_custom_shell 4 | apt: 5 | name: zsh 6 | state: latest 7 | register: zsh_installed 8 | 9 | - name: Clone oh-my-zsh 10 | when: enable_custom_shell 11 | git: 12 | repo: https://github.com/robbyrussell/oh-my-zsh.git 13 | dest: /home/{{ user }}/.oh-my-zsh 14 | register: oh_my_zsh_cloned 15 | 16 | - name: Set correct owner for oh-my-zsh files 17 | when: oh_my_zsh_cloned|succeeded 18 | file: 19 | path: /home/{{ user }}/.oh-my-zsh/ 20 | owner: "{{ user }}" 21 | group: "{{ user }}" 22 | mode: u=rwX,g=rX,o=rX 23 | recurse: yes 24 | 25 | - name: Download custom oh-my-zsh theme 26 | when: oh_my_zsh_cloned|succeeded 27 | get_url: 28 | url: https://gist.githubusercontent.com/lukeharvey/992fca16c6f5d1b66f75b9e1b17ab308/raw/guru2.zsh-theme 29 | dest: /home/{{ user }}/.oh-my-zsh/themes/guru2.zsh-theme 30 | owner: "{{ user }}" 31 | group: "{{ user }}" 32 | mode: 0644 33 | 34 | - name: Download z.sh 35 | when: enable_custom_shell 36 | get_url: 37 | url: https://raw.githubusercontent.com/rupa/z/master/z.sh 38 | dest: /home/{{ user }}/z.sh 39 | owner: "{{ user }}" 40 | group: "{{ user }}" 41 | mode: 0644 42 | 43 | - name: Copy over zshrc 44 | when: enable_custom_shell 45 | template: 46 | src: ./src/zsh/zshrc.j2 47 | dest: /home/{{ user }}/.zshrc 48 | owner: "{{ user }}" 49 | group: "{{ user }}" 50 | mode: 0644 51 | 52 | - name: Set zsh as default shell 53 | when: zsh_installed|succeeded 54 | user: 55 | name: "{{ user }}" 56 | shell: /bin/zsh 57 | -------------------------------------------------------------------------------- /src/fail2ban/jail.local: -------------------------------------------------------------------------------- 1 | # Fail2Ban configuration file. 2 | 3 | [DEFAULT] 4 | 5 | ignoreip = 127.0.0.1/8 6 | bantime = 86400 ; 1 day 7 | findtime = 86400 ; 1 day 8 | maxretry = 3 9 | 10 | backend = auto 11 | 12 | usedns = warn 13 | 14 | destemail = root@localhost 15 | sendername = Fail2Ban 16 | banaction = iptables-multiport 17 | mta = sendmail 18 | protocol = tcp 19 | chain = INPUT 20 | 21 | action_ = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] 22 | 23 | action_mw = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] 24 | %(mta)s-whois[name=%(__name__)s, dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s", sendername="%(sendername)s"] 25 | 26 | action_mwl = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] 27 | %(mta)s-whois-lines[name=%(__name__)s, dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s", sendername="%(sendername)s"] 28 | 29 | action = %(action_)s 30 | 31 | # 32 | # SSH 33 | # 34 | 35 | [ssh] 36 | 37 | enabled = true 38 | port = ssh 39 | filter = sshd 40 | logpath = /var/log/auth.log 41 | 42 | [ssh-ddos] 43 | 44 | enabled = true 45 | port = ssh 46 | filter = sshd-ddos 47 | logpath = /var/log/auth.log 48 | 49 | # 50 | # HTTP servers 51 | # 52 | 53 | [php-url-fopen] 54 | 55 | enabled = false 56 | port = http,https 57 | filter = php-url-fopen 58 | logpath = /var/log/nginx/*access.log 59 | 60 | [nginx-http-auth] 61 | 62 | enabled = false 63 | port = http,https 64 | filter = nginx-http-auth 65 | logpath = /var/log/nginx/*error.log 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ansible Playbook for Initial Server Setup with Ubuntu 14.04 2 | 3 | An [Ansible](http://docs.ansible.com/) playbook which runs a series of configuration steps to increase the security and usability of a new Ubuntu 14.04 server, in order to provide a solid foundation for subsequent actions. 4 | 5 | It borrows heavily from the work of: [Bryan Kennedy](https://plusbryan.com/my-first-5-minutes-on-a-server-or-essential-security-for-linux-servers), [Ryan Eschinger](http://ryaneschinger.com/blog/securing-a-server-with-ansible/), [Ashley Rich](https://github.com/A5hleyRich/wordpress-ansible), and [Digital Ocean](https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-14-04) 6 | 7 | It will perform the following: 8 | * Create a new "super user" with root privileges and public key authentication 9 | * Implement several SSH hardening techniques 10 | * Configure a basic firewall 11 | * Configure the timezone and enable time synchronization 12 | * Modify the hostname and hosts file 13 | * Enable unattended upgrades 14 | * Install Fail2Ban 15 | 16 | Optional extras: 17 | * Install New Relic Server monitoring 18 | * Install customised shell with Zsh and oh-my-zsh 19 | 20 | ## Requirements 21 | 22 | * [Ansible](http://docs.ansible.com/ansible/intro_installation.html) installed locally on your machine 23 | * Root SSH access to a new Ubuntu 14.04 VPS 24 | 25 | ## Configuration 26 | 27 | Clone the repo 28 | 29 | ``` 30 | $ git clone https://github.com/lukeharvey/ansible-initial-server-setup.git 31 | ``` 32 | 33 | Modify the variables in **_vars/main.yml_** according to your needs: 34 | 35 | **user:** the username for your new "super user" 36 | 37 | **password:** a [hashed](http://docs.ansible.com/ansible/faq.html#how-do-i-generate-crypted-passwords-for-the-user-module) sudo password 38 | 39 | **public_key:** the local path to your public SSH key 40 | 41 | **fqdn:** your chosen [FQDN](https://kb.iu.edu/d/aiuv) 42 | 43 | **hostname:** your chosen hostname 44 | 45 | **timezone:** the most appropriate [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for your server 46 | 47 | **ssh_port:** your chosen SSH port 48 | 49 | **enable_newrelic:** change to `no` to disable installation of New Relic Server monitor, which requires a [New Relic license key](https://docs.newrelic.com/docs/accounts-partnerships/accounts/account-setup/license-key) 50 | 51 | **newrelic_license_key:** your [New Relic license key](https://docs.newrelic.com/docs/accounts-partnerships/accounts/account-setup/license-key) 52 | 53 | **enable_custom_shell:** change to `no` to disable installation of customised shell with Zsh and oh-my-zsh 54 | 55 | ## Testing 56 | 57 | A Vagrantfile is provided which allows the playbook to be tested locally. 58 | 59 | You will need Vagrant and Virtualbox installed, and you should uncomment the following lines in **_ansible.cfg_** to makes things work nicely. 60 | ``` 61 | # private_key_file = ~/.vagrant.d/insecure_private_key 62 | # host_key_checking = False 63 | ``` 64 | Then run `$ vagrant up` to generate your virtual machine. 65 | 66 | And then run the playbook: 67 | 68 | `$ ansible-playbook test_server_setup.yml` 69 | 70 | ## Production 71 | 72 | Add your server's IP address to the **_hosts_** file 73 | 74 | ``` 75 | [production] 76 | # e.g 192.168.1.1 77 | # or host.example.com 78 | ``` 79 | 80 | Then run the playbook: 81 | 82 | `$ ansible-playbook initial_server_setup.yml --ask-pass` 83 | --------------------------------------------------------------------------------