Compare commits

...

2 Commits

Author SHA1 Message Date
7bd4858c7e
Add DNS-01 ACME wildcard certificate
- Add Ansible Vault convenience script
2022-11-19 20:58:07 -05:00
e7a8c8aa1c
Add port forward script and WordPress
- Added Makefile
- Added UFW firewall
2022-11-19 05:02:28 -05:00
15 changed files with 286 additions and 10 deletions

3
.gitignore vendored
View File

@ -1,4 +1,7 @@
.ansible_vault
.bitwarden
environments environments
*.log
.playbook .playbook
.vagrant .vagrant
.vscode .vscode

9
Makefile Normal file
View File

@ -0,0 +1,9 @@
all: vagrant
vagrant:
vagrant up --no-destroy-on-error --no-color | tee ./vagrantup.log
./scripts/forward-ssh.sh
clean:
vagrant destroy -f --no-color
rm -rf .vagrant ./*.log

View File

@ -1,15 +1,29 @@
# Free I.T. Athen's Infrastructure # Free I.T. Athen's Infrastructure
This project is used to develop Ansible for deploying and maintaining websites This project is used to develop Ansible for deploying and maintaining websites
and services operated by Free I.T. Athens. and services operated by Free I.T. Athens (FRITA).
- Requires Ansible and Vagrant on the host - Requires GNU Make, Ansible, and Vagrant on the host
## Quick Start ## Quick Start
1. Clone this project 1. Clone this project
2. Run `vagrant up` to provision a Debian 11 base box 2. Run `make` to provision a Debian 11 base box
3. Go to
- [Traefik Dashboard](https://traefik.local.freeitathens.org:8443/dashboard/#/)
- [WordPress](https://www.local.freeitathens.org)
4. Click through the HTTPS security warning
## Production
1. Clone [production-env](https://github.com/freeitathens/production-env/) to `./environments`
2. Run `./scripts/vault-key.sh` from the root of the project to obtain the Ansible Vault password
3. Enter the Bitwarden Master Password
4. Run `ansible-playbook` against the production servers, e.g.,
```
ansible-playbook -u root -i environments/production --vault-pass-file ./.ansible_vault webserver.yml --check
```
5. Delete the `.ansible_vault` file when you are done
## Authors ## Authors
* **Kris Lamoureux** - *Project Founder* - @[krislamo](https://github.com/krislamo) * **Kris Lamoureux** - *Project Founder* - [@krislamo](https://github.com/krislamo)
## Copyrights and Licenses ## Copyrights and Licenses
Copyright (C) 2019, 2020, 2022 Free I.T. Athens Copyright (C) 2019, 2020, 2022 Free I.T. Athens

View File

@ -1,5 +1,46 @@
###############
### Secrets ###
###############
secret:
WORDPRESS_DB_PASSWORD: WPpa55w0rd!
##############
### Docker ###
##############
docker_users: docker_users:
- vagrant - vagrant
webserver_env: ################
TRAEFIK_DOMAIN: traefik.example.org #### MariaDB ###
################
databases:
- name: wordpress
pass: "{{ secret.WORDPRESS_DB_PASSWORD }}"
#######################
### Webserver Stack ###
#######################
webserver:
###############
### Traefik ###
###############
#TRAEFIK_VERSION: latest
#TRAEFIK_ROOT_DOMAIN: local.freeitathens.org
#TRAEFIK_DOMAIN: traefik.local.freeitathens.org
#TRAEFIK_DASHBOARD: true
#TRAEFIK_EXPOSED_DEFAULT: false
#TRAEFIK_WEB_ENABLED: true
TRAEFIK_DEBUG: true
TRAEFIK_ACME_PROVIDER: dreamhost
TRAEFIK_ACME_CASERVER: https://acme-v02.api.letsencrypt.org/directory
TRAEFIK_ACME_EMAIL: frita@example.org
#################
### WordPress ###
#################
#WORDPRESS_VERSION: latest
#WORDPRESS_DOMAIN: www.local.freeitathens.org
#WORDPRESS_DB_HOST: host.docker.internal
#WORDPRESS_DB_NAME: wordpress
#WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: "{{ secret.WORDPRESS_DB_PASSWORD }}"

View File

@ -4,5 +4,6 @@
vars_files: vars_files:
- vars/webserver.yml - vars/webserver.yml
roles: roles:
- common
- docker - docker
- webserver - webserver

View File

@ -0,0 +1,30 @@
- name: Create Ansible's temporary remote directory
ansible.builtin.file:
path: "~/.ansible/tmp"
state: directory
mode: 0700
- name: Install the Uncomplicated Firewall
ansible.builtin.apt:
name: ufw
state: present
update_cache: true
- name: Deny incoming traffic by default
community.general.ufw:
default: deny
direction: incoming
- name: Allow outgoing traffic by default
community.general.ufw:
default: allow
direction: outgoing
- name: Allow OpenSSH with rate limiting
community.general.ufw:
name: ssh
rule: limit
- name: Enable firewall
community.general.ufw:
state: enabled

View File

@ -2,7 +2,6 @@
ansible.builtin.apt: ansible.builtin.apt:
name: ['docker.io', 'docker-compose'] name: ['docker.io', 'docker-compose']
state: present state: present
update_cache: true
- name: Create docker-compose root - name: Create docker-compose root
ansible.builtin.file: ansible.builtin.file:

View File

@ -1 +1,4 @@
webserver_root: "{{ docker_compose_root }}/webserver" webserver_root: "{{ docker_compose_root }}/webserver"
mariadb_trust:
- "172.16.0.0/12"
- "192.168.0.0/16"

View File

@ -1,5 +1,8 @@
version: '3.5' version: '3.5'
volumes:
wordpress:
networks: networks:
traefik: traefik:
name: traefik name: traefik
@ -7,26 +10,67 @@ networks:
services: services:
traefik: traefik:
image: traefik:${TRAEFIK_VERSION:-latest} image: traefik:${TRAEFIK_VERSION:-latest}
restart: always
command: command:
- --api.dashboard=${TRAEFIK_DASHBOARD:-true} - --api.dashboard=${TRAEFIK_DASHBOARD:-true}
- --api.debug=${TRAEFIK_DEBUG:-false} - --api.debug=${TRAEFIK_DEBUG:-false}
- --log.level=${TRAEFIK_LOG_LEVEL:-ERROR}
- --providers.docker=true - --providers.docker=true
- --providers.docker.exposedbydefault=${TRAEFIK_EXPOSED_DEFAULT:-false}
- --entrypoints.web.address=:80 - --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.local.address=:8443
- --entrypoints.web.http.redirections.entrypoint.to=websecure - --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https - --entrypoints.web.http.redirections.entrypoint.scheme=https
- --entrypoints.web.http.redirections.entrypoint.permanent=true - --entrypoints.web.http.redirections.entrypoint.permanent=true
- --entrypoints.websecure.address=:443 - --certificatesresolvers.letsencrypt.acme.email=${TRAEFIK_ACME_EMAIL}
- --entrypoints.local.address=:8443 - --certificatesresolvers.letsencrypt.acme.storage=/etc/letsencrypt/acme.json
- --certificatesresolvers.letsencrypt.acme.dnschallenge=true
- --certificatesresolvers.letsencrypt.acme.dnschallenge.provider=${TRAEFIK_ACME_PROVIDER}
- --certificatesresolvers.letsencrypt.acme.dnschallenge.delaybeforecheck=0
- --certificatesresolvers.letsencrypt.acme.caserver=${TRAEFIK_ACME_CASERVER:-https://acme-staging-v02.api.letsencrypt.org/directory}
environment:
DREAMHOST_API_KEY: ${TRAEFIK_DREAMHOST_APIKEY}
ports: ports:
- 80:80 - 80:80
- 443:443 - 443:443
- "127.0.0.1:8443:8443" - "127.0.0.1:8443:8443"
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ./.acme:/etc/letsencrypt
labels: labels:
traefik.http.routers.api.rule: Host(`${TRAEFIK_DOMAIN:-traefik.local.freeitathens.org}`) traefik.http.routers.api.rule: Host(`${TRAEFIK_DOMAIN:-traefik.local.freeitathens.org}`)
traefik.http.routers.api.entrypoints: local traefik.http.routers.api.entrypoints: local
traefik.http.routers.api.service: api@internal traefik.http.routers.api.service: api@internal
traefik.http.routers.api.tls: true traefik.http.routers.api.tls: true
traefik.http.routers.api.tls.certresolver: letsencrypt
traefik.http.routers.api.tls.domains[0].main: ${TRAEFIK_ACME_DOMAIN_MAIN:-local.freeitathens.org}
traefik.http.routers.api.tls.domains[0].sans: "${TRAEFIK_ACME_DOMAIN_SANS:-*.local.freeitathens.org}"
traefik.enable: ${TRAEFIK_WEB_ENABLED:-true}
networks: networks:
- traefik - traefik
wordpress:
image: wordpress:${WORDPRESS_VERSION:-latest}
restart: always
environment:
WORDPRESS_DB_HOST: ${WORDPRESS_DB_HOST:-host.docker.internal}
WORDPRESS_DB_NAME: ${WORDPRESS_DB_NAME-wordpress}
WORDPRESS_DB_USER: ${WORDPRESS_DB_USER:-wordpress}
WORDPRESS_DB_PASSWORD: ${WORDPRESS_DB_PASSWORD}
labels:
traefik.http.routers.wordpress.rule: Host(`${WORDPRESS_DOMAIN:-www.local.freeitathens.org}`)
traefik.http.routers.wordpress.entrypoints: websecure
traefik.http.routers.wordpress.tls: true
traefik.http.routers.wordpress.tls.certresolver: letsencrypt
traefik.http.routers.wordpress.tls.domains[0].main: ${TRAEFIK_ACME_DOMAIN_MAIN:-local.freeitathens.org}
traefik.http.routers.wordpress.tls.domains[0].sans: "${TRAEFIK_ACME_DOMAIN_SANS:-*.local.freeitathens.org}"
traefik.http.services.wordpress.loadbalancer.server.port: 80
traefik.docker.network: traefik
traefik.enable: ${WORDPRESS_WEB_ENABLED:-true}
volumes:
- wordpress:/var/www/html
networks:
- traefik
extra_hosts:
- host.docker.internal:host-gateway

View File

@ -3,3 +3,9 @@
args: args:
chdir: "{{ webserver_root }}" chdir: "{{ webserver_root }}"
listen: composeup_webserver listen: composeup_webserver
- name: Restart MariaDB
ansible.builtin.service:
name: mariadb
state: restarted
listen: restart_mariadb

View File

@ -3,6 +3,37 @@
name: mariadb-server name: mariadb-server
state: present state: present
- name: Change the bind-address to allow Docker
ansible.builtin.lineinfile:
path: /etc/mysql/mariadb.conf.d/50-server.cnf
regex: "^bind-address"
line: "bind-address = 0.0.0.0"
notify: restart_mariadb
- name: Install MySQL Support for Python 3
ansible.builtin.apt:
name: python3-pymysql
state: present
- name: Create MariaDB databases
community.mysql.mysql_db:
name: "{{ item.name }}"
state: present
login_unix_socket: /var/run/mysqld/mysqld.sock
loop: "{{ databases }}"
no_log: "{{ item.pass is defined }}"
- name: Create MariaDB users
community.mysql.mysql_user:
name: "{{ item.name }}"
password: "{{ item.pass }}"
host: '%'
state: present
priv: "{{ item.name }}.*:ALL"
login_unix_socket: /var/run/mysqld/mysqld.sock
loop: "{{ databases }}"
no_log: "{{ item.pass is defined }}"
- name: Create webserver docker-compose directory - name: Create webserver docker-compose directory
ansible.builtin.file: ansible.builtin.file:
path: "{{ webserver_root }}" path: "{{ webserver_root }}"
@ -22,3 +53,20 @@
dest: "{{ webserver_root }}/.env" dest: "{{ webserver_root }}/.env"
mode: 0600 mode: 0600
notify: composeup_webserver notify: composeup_webserver
- name: Allow MariaDB database connections
community.general.ufw:
rule: allow
port: 3306
proto: tcp
src: "{{ item }}"
loop: "{{ mariadb_trust }}"
- name: Add HTTP and HTTPS firewall rule
community.general.ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop:
- "80"
- "443"

View File

@ -1,4 +1,4 @@
# {{ ansible_managed }} # {{ ansible_managed }}
{% for key, value in webserver_env.items() %} {% for key, value in webserver.items() %}
{{ key }}={{ value }} {{ key }}={{ value }}
{% endfor %} {% endfor %}

26
scripts/forward-ssh.sh Executable file
View File

@ -0,0 +1,26 @@
#!/bin/bash
# Finds the SSH private key under ./.vagrant and connects to
# the Vagrant box, port forwarding localhost ports: 8443, 80, 443
PRIVATE_KEY="$(find .vagrant -name "private_key")"
HOST_IP="$(vagrant ssh -c "hostname -I | cut -d' ' -f2" 2>/dev/null)"
MATCH_PATTERN="ssh -fNT -i ${PRIVATE_KEY}.*vagrant@"
function ssh_connect {
sudo ssh -fNT -i "$PRIVATE_KEY" \
-L 8443:localhost:8443 \
-L 80:localhost:80 \
-L 443:localhost:443 \
-o UserKnownHostsFile=/dev/null \
-o StrictHostKeyChecking=no \
vagrant@"${HOST_IP::-1}" 2>/dev/null
}
set -x
if [ "$(pgrep -afc "$MATCH_PATTERN")" -eq 0 ]; then
ssh_connect
else
pgrep -f "$MATCH_PATTERN" | xargs sudo kill -9
ssh_connect
fi
set +x

51
scripts/vault-key.sh Executable file
View File

@ -0,0 +1,51 @@
#!/bin/bash
BW_USERNAME="contact@freeitathens.org"
ANSIBLE_VAULT_ITEM="e16b2542-f6c1-4e9f-8e33-af5201574a15"
# Does the key already exist?
if [ -f .ansible_vault ]; then
echo "Ansible Vault file already exists at ./.ansible_vault"
exit 1
fi
# Install Bitwarden CLI binary to ./.bitwarden/bw
if [ ! -d .bitwarden ]; then
mkdir .bitwarden
cd .bitwarden || exit 1
wget "https://vault.bitwarden.com/download/?app=cli&platform=linux" -O bw-linux.zip
unzip bw-linux.zip
rm bw-linux.zip
chmod u+x bw
else
cd .bitwarden || exit 1
fi
# Get Master Password to unlock vault
read -rsp "Master Password: " BW_PASSWORD
export BW_PASSWORD
echo
# Login
LOGIN_RESPONSE=$(./bw login "$BW_USERNAME" "$BW_PASSWORD" --response --nointeraction)
if [ ! "$(echo "$LOGIN_RESPONSE" | jq -r .success)" == "true" ]; then
echo "$LOGIN_RESPONSE" | jq -r .message
exit 1
fi
# Unlock
UNLOCK_RESPONSE=$(./bw unlock --passwordenv BW_PASSWORD --response --nointeraction)
if [ ! "$(echo "$UNLOCK_RESPONSE" | jq -r .success)" == "true" ]; then
echo "$UNLOCK_RESPONSE" | jq -r .message
exit 1
fi
# Trade password for session
unset BW_PASSWORD
BW_SESSION=$(echo "$UNLOCK_RESPONSE" | jq -r .data.raw)
export BW_SESSION
# Place Ansible Vault secret and logout
./bw get password "$ANSIBLE_VAULT_ITEM" --response --nointeraction | jq -r .data.data > ../.ansible_vault
truncate -s -1 ../.ansible_vault
chmod 600 ../.ansible_vault
./bw logout --quiet

View File

@ -2,5 +2,6 @@
hosts: all hosts: all
become: true become: true
roles: roles:
- common
- docker - docker
- webserver - webserver