initial commit

master
HgO 2020-04-13 14:46:45 +02:00
commit 961498e32b
76 changed files with 2715 additions and 0 deletions

1
.gitignore vendored 100644
View File

@ -0,0 +1 @@
.vscode

View File

@ -0,0 +1,45 @@
openssh_port: "12322"
smtp_accounts:
- name: ahoy
host: mail.infomaniak.ch
port: 587
from: ahoy@pirateparty.be
password: !vault |
$ANSIBLE_VAULT;1.1;AES256
62633164383764376333643063363263356461613164663066623836613931306437633033633134
3632326164663564653962613437376265333234313032360a313935303230393938356632356231
34613661383736313232613131313262616261323464653936393634653464323631333839353030
3230396536663635650a633537633633623365346563323334616338333436633537623831313165
38343766346437626332313230346537663735313937643765353465356236633134
smtp_default_account: ahoy
smtp_default_contact: it@pirateparty.be
node_exporter_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
35333237666635633862336433303264613133376230346461333332636231643563636466356630
3235623237303366626562393065353436663632306438370a333132653432636632643134326130
66396666626631373637373065613137393232383361346438633763396266636264663364663238
3363666332633562360a323532666664333266333761343136306133336138623137316234653939
37643239613631383165656138633134663736393238343939336135303732333838336538373531
3635396265643061356339333035393836313936316633623662
backup_targets:
- host: storage.pirateparty.be
ssh:
port: 23
username: "{{ storage_box_username }}"
key_file: storage-box
- host: batato.be
ssh:
key_file: batato
users:
- name: hgo
ssh_keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOK8Y3OEq1j3rR8EOLpVPYZeA5qC0PTsctza9c2qhbU hadrien@terry
- name: tierce
ssh_keys:
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC75IeAMEg6RwvbO6oLEQOpYSASGx9A3JD15gtA7D3NJFz+pZ7hBjBSjTxZrHDQLg1OFs0XRGS5DATRMnj6jSRAc25C71DewbNY9fWOH1/dAuo45zBllO3/pol17uYVqUbaPVjnqQFfikLCf7HjBbjt7JEVffJ3nkalE2q0TqjGK0JrltrL9dePE/R3ZNzVSDXvkgMsu18Wov9if6ftsKYgNTW+oOc9xoN1GSHYEnzv68+YNt3zKGTiwhU87cLyHJBu9o/wFDNOLdQcOtKa3IUPZvOgDlLrAm8a4Z9/A9DCJS/8kFmyNOazF1rupPAojn7k9mIBvVPxc5zqg+qrKbxR tierce@q
acme_email: it@pirateparty.be

View File

@ -0,0 +1,41 @@
alertmanager_smtp:
from: ahoy@pirateparty.be
smarthost: mail.infomaniak.ch:587
auth_username: ahoy@pirateparty.be
auth_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
61643536623562333434653364623535633331653539356132653863313965313030333163313637
6161333463653839383265323937376630336134633531650a313132326536346530353764656465
63323737643034353532333363303363616261363335333365663133626537653961323133626433
6566656236383864610a323262393562663836343162326131336630363939356333313934326436
6261
alertmanager_route:
receiver: 'default-receiver'
group_wait: 30s
group_interval: 5m
repeat_interval: 3h
alertmanager_receivers:
- name: default-receiver
email_configs:
- to: hadrien@pirateparty.be
alertmanager_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
63313035633034643636326230383162666666626539623934303631366236656432616238356362
6665626364643666343737623532616133303539356133300a396530643865323334313564363762
31646562306232356437636537383732626664663166656331303630303537383064663565323235
3962313936613039320a656337356131363636643366393233613462313361323639373363643134
32383436313035323032656266376664383166633631663438316165313930373937636436633962
6131336262343531643264346362343433373165386266323439
prometheus_password: "{{ alertmanager_password }}"
grafana_admin_user: ppbe
grafana_admin_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
39626461326636633230343536613564643537376464336537353661636638303238303966383030
3133663938623334396435333761306265373064353462610a356531326130396566386638653533
36323833663030663466356538353237376137313135656534383439613935623234373065376530
3864366438626135300a333664313339343964306538343366306639393631666366323537313734
36613731626439646537653565646436323839383930363131653431306431396638613665616464
3435313137313964636139366439336365663564326639303234

View File

@ -0,0 +1,46 @@
umurmur_version: 0.2.17
umurmur_domain: mumble.parley.be
umurmur_welcome_text:
- Welcome to Parley Talk!
- You can talk to the people in the room you joined.
- You start in the Welcome room, to join another channel right click the room name and choose "Join Channel"
umurmur_admin_password: wC7yZ4vV2ocb7AkBfQ2RwuhRqYVyiwY42Rjpw3pfJ
umurmur_max_users: 100
umurmur_channels:
- name: Parley
description: Main Parley Talk channel. No entry.
noenter: yes
- name: Welcome
parent: Parley
description: Welcome channel
position: 0
- name: Silent
parent: Parley
description: Silent channel
silent: yes
position: 1
- name: Mary Read's territory
description: The channel dedicated to Mary Read. She was a Caribbean pirate. Dressed as a man, Mary went to sea and later joined the British army, fighting in the War Of The Spanish Succession. She married and settled down as a woman, but dressed back as a man following the death of her husband, later boarding a ship bound for the West Indies.
parent: Welcome
- name: Anne Bonny crew
description: The channel dedicated to Anne Bonny. She was one the most famous female pirates. She operated in the Caribbean. She discovered that one of her crew companion, Mark Read, was secretly a woman (Mary Read) and the two became very close.
parent: Welcome
- name: Mary Cricket's ship
description: The channel dedicated to Mary Cricket. Toghether with 5 other prisoners Mary Crickett escaped and overpowered the two-man crew of the sloop John and Elizabeth on 12 May 1729. She held the prisoners in the ship's hold, sitting on the hatch to prevent their escape. The pair was released a few days later. The pirates sailed into Chesapeake Bay but before they could raid any other ships, they were captured by HMS Shoreham. Returned to Virginia, they were tried in August 1729, convicted of piracy, and sentenced to hang.
parent: Welcome
- name: Flora Burn's island
description: The channel dedicated to Flora Burn. She began her pirate career in 1741 and operated mainly on the East Coast of North America.
parent: Welcome
- name: Sayyida al Hurra
description: She was a Moroccan pirate from the 16th century, and controlled the Mediterranean Sea together with Barbarossa. Her name means "noble lady who is free and independent"
parent: Welcome
- name: Ching Shih
description: She was a Chinese pirate from the 19th century. She is considered to be the pirate with the largest crew ever assembled (between 20.000 and 40.000 pirates) and died peacefully as a free woman
parent: Welcome
umurmur_default_channel: Welcome
mumble_web_domain: talk.parley.be
mumble_web_version: c03b78d096eae69e1cec82e148f65e7a8541bd68

View File

@ -0,0 +1 @@
ansible_user: admin

View File

@ -0,0 +1,8 @@
storage_box_username: u212275-sub5
storage_box_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
36313662333062323531613966386365373339663566303133653562663838316632613830613264
6564333736343830623061313534313630313534316231390a666662633861383563333562356561
64616534313266323833383331313334333761323965383634666635663430366461353437616465
6337363536643738310a656530663837386537336434633037376463336165613239323265366234
64663863333763356430616635323061396663373264343666323831646664646430

View File

@ -0,0 +1,11 @@
[web]
mastodon.pirateparty.be
pirateparty.be
wiki.pirateparty.be
talk.parley.be
[monitoring]
status.pirateparty.be
[mumble]
talk.parley.be

View File

@ -0,0 +1,12 @@
---
- hosts: all
become: yes
pre_tasks:
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
roles:
- common

View File

@ -0,0 +1,4 @@
- hosts: monitoring
roles:
- common
- monitoring

View File

@ -0,0 +1,10 @@
- hosts: mumble
become: yes
vars:
acme_domains:
- "{{ umurmur_domain }}"
- "{{ mumble_web_domain }}"
roles:
- acme
- mumble

View File

@ -0,0 +1,9 @@
acme_directory: https://acme-v02.api.letsencrypt.org/directory
acme_config_dir: /etc/ssl
acme_keys_dir: "{{ acme_config_dir }}/private"
acme_csr_dir: "{{ acme_config_dir }}/csr"
acme_certs_dir: "{{ acme_config_dir }}/certs"
acme_accounts_dir: "{{ acme_config_dir }}/accounts"
acme_account_key: "acme_account.key"
acme_ssl_group: ssl-cert
acme_challenge_dir: /var/www/acme

View File

@ -0,0 +1,184 @@
#!/usr/bin/python3
import logging
import os
import sys
from datetime import datetime
import josepy as jose
from OpenSSL import crypto
from acme import client, messages, challenges, crypto_util
SSL_CONFIG_DIR="/etc/ssl"
OPENSSL_DATE_FORMAT = '%Y%m%d%H%M%SZ'
USER_AGENT="python-acme"
def select_http01_challenge(order):
for auth in order.authorizations:
for challenge in auth.body.challenges:
if isinstance(challenge.chall, challenges.HTTP01):
return challenge
raise Exception("HTTP-01 challenge was not offered by the CA server.")
def perform_http01_challenge(client_acme, challenge, order, challenge_dir):
response, validation = challenge.response_and_validation(client_acme.net.key)
validation_filename, validation_content = validation.split('.')
challenge_path = os.path.join(challenge_dir, validation_filename)
with open(challenge_path, 'w') as f_out:
f_out.write(validation_content)
client_acme.answer_challenge(challenge, response)
fullchain_pem = client_acme.poll_and_finalize(order).fullchain_pem
os.remove(challenge_path)
return fullchain_pem
def renew_cert(domain,
account_key_path,
privkey_path,
csr_path,
certs_dir,
challenge_dir,
directory_url,
days_before_renewal,
force = False):
logging.info(f"Checking {domain} certificate's expiration date…")
now = datetime.now()
cert_expiration_date = now
fullchain_path = os.path.join(certs_dir, "fullchain.pem")
try:
with open(fullchain_path, 'rb') as pem_in:
fullchain_pem = crypto.load_certificate(crypto.FILETYPE_PEM, pem_in.read())
cert_expiration_date = datetime.strptime(fullchain_pem.get_notAfter().decode(), OPENSSL_DATE_FORMAT)
except IOError as e:
logging.warning(f"Unable to load {domain} certificate: {e}")
cert_expiration_days = (cert_expiration_date - now).days
if not force and cert_expiration_days >= days_before_renewal:
logging.info(f"Certificate expires in {cert_expiration_days} days. Nothing to do.")
return
logging.info(f"Certificate expires in {cert_expiration_days} days! Renewing certificate…")
logging.info(f"Loading account key from {account_key_path}")
with open(account_key_path, 'rb') as pem_in:
account_pem = crypto.load_privatekey(crypto.FILETYPE_PEM, pem_in.read())
account_jwk = jose.JWKRSA(key=account_pem.to_cryptography_key())
account_jwk_pub = account_jwk.public_key()
logging.info(f"Loading {domain} CSR file from {csr_path}")
try:
with open(csr_path, 'r') as pem_in:
csr_pem = pem_in.read()
except IOError as e:
logging.warning(f"Unable to load CSR file: {e}")
logging.info(f"Loading {domain} private key from {privkey_path}")
with open(privkey_path, 'r') as pem_in:
privkey_pem = pem_in.read()
csr_pem = crypto_util.make_csr(privkey_pem, [domain])
client_network = client.ClientNetwork(account_jwk, user_agent=USER_AGENT)
directory = messages.Directory.from_json(client_network.get(directory_url).json())
client_acme = client.ClientV2(directory, net=client_network)
logging.info("Registering with ACME account…")
# Here we assume we already have an account
registration_message = messages.NewRegistration(
key=account_jwk_pub,
only_return_existing=True)
registration_response = client_acme._post(directory["newAccount"], registration_message)
account = client_acme._regr_from_response(registration_response)
client_acme.net.account = account
logging.info("Ordering ACME challenge…")
order = client_acme.new_order(csr_pem)
logging.info("Selecting HTTP-01 ACME challenge…")
challenge = select_http01_challenge(order)
logging.info("Performing HTTP-01 ACME challenge…")
fullchain_pem = perform_http01_challenge(client_acme, challenge, order, challenge_dir)
logging.info(f"Writing {domain} certificates into {certs_dir}")
certs_pem = []
for line in fullchain_pem.split('\n'):
if 'BEGIN CERTIFICATE' in line:
cert_pem = ''
cert_pem += line + '\n'
if 'END CERTIFICATE' in line:
certs_pem.append(cert_pem)
cert_path = os.path.join(certs_dir, "cert.pem")
with open(cert_path, 'w') as pem_out:
pem_out.write(certs_pem[0])
chain_path = os.path.join(certs_dir, "chain.pem")
with open(chain_path, 'w') as pem_out:
pem_out.write(''.join(certs_pem[1:]))
with open(fullchain_path, 'w') as pem_out:
pem_out.write(fullchain_pem)
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(
description="Renew ACME certificates for a list of domains. This script assumes you already have an account on the CA server and a private key for each certificate.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("domains", nargs='+',
help="List of domain names for which to renew the certificate.")
parser.add_argument("--account_key", "-a",
default=os.path.join(SSL_CONFIG_DIR, "accounts", "acme_account.key"),
help="Path to the account key.")
parser.add_argument("--privkey", "-p",
default=os.path.join(SSL_CONFIG_DIR, "private", "{domain}.pem"),
help="Path to the private certificate.")
parser.add_argument("--csr", "-r",
default=os.path.join(SSL_CONFIG_DIR, "csr", "{domain}.pem"),
help="Path to the CSR file. If the file doesn't exist, it will be generated from the private key and the domain name.")
parser.add_argument("--certs", "-o",
default=os.path.join(SSL_CONFIG_DIR, "certs", "{domain}.d"),
help="Path to the certificates directory.")
parser.add_argument("--challenge", "-c",
default="/var/www/html/.well-known/acme-challenge",
help="Path to the challenge directory.")
parser.add_argument("--directory_url", "-u",
default="https://acme-v02.api.letsencrypt.org/directory",
help="Directory URL on which performing ACME challenges. Only ACME v2 is supported.")
parser.add_argument("--days", "-d", type=int,
default=30,
help="Days before attempting to renew the certificates.")
parser.add_argument("--quiet", "-q",
action="store_true",
help="Quiet mode.")
parser.add_argument("--force", "-f",
action="store_true",
help="Force certificates renewal without checking their expiration date.")
args = parser.parse_args()
log_level = logging.WARN if args.quiet else logging.INFO
logging.basicConfig(stream=sys.stdout, level=log_level, format="%(levelname)s:%(message)s")
for domain in args.domains:
account_key = args.account_key.format(domain=domain)
privkey = args.privkey.format(domain=domain)
csr = args.csr.format(domain=domain)
certs_dir = args.certs.format(domain=domain)
challenge_dir = args.challenge.format(domain=domain)
renew_cert(domain,
account_key_path=account_key,
privkey_path=privkey,
csr_path=csr,
certs_dir=certs_dir,
challenge_dir=challenge_dir,
directory_url=args.directory_url,
days_before_renewal=args.days,
force=args.force)

View File

@ -0,0 +1,76 @@
- name: Install ACME dependencies
apt:
name: python3-acme
state: present
tags: acme_install
- name: Create Let's Encrypt config directories
file:
path: "{{ config_dir }}"
state: directory
owner: root
group: "{{ acme_ssl_group }}"
mode: "711"
loop:
- "{{ acme_config_dir }}"
- "{{ acme_keys_dir }}"
- "{{ acme_accounts_dir }}"
- "{{ acme_csr_dir }}"
loop_control:
loop_var: config_dir
tags: acme_install
- name: Create challenge directory
file:
path: "{{ acme_challenge_dir }}/.well-known/acme-challenge"
state: directory
owner: root
group: root
mode: "755"
tags: acme_install
- name: Perform ACME challenge for each domain
include_tasks:
file: challenge.yml
apply:
tags: acme_challenge
loop: "{{ acme_domains | unique }}"
loop_control:
loop_var: domain_name
tags: acme_challenge
- name: Create directory for certificate renewal tool
file:
path: /opt/acme
owner: root
group: root
mode: "755"
state: directory
tags: acme_renew
- name: Copy script to renew ACME certificates
copy:
src: acme_renew_cert.py
dest: /opt/acme/acme_renew_cert.py
owner: root
group: root
mode: "755"
tags: acme_renew
- name: Setup cron job for ACME certificates renewal of {{ domain_name }}
cron:
name: acme renew {{ domain_name }} cert
job: >-
sleep $((RANDOM % 3600)) && /opt/acme/acme_renew_cert.py {{ domain_name }} -q
-a {{ (acme_accounts_dir + '/' + acme_account_key) | quote }}
-p {{ acme_keys_dir | quote }}/{domain}.pem
-r {{ acme_csr_dir | quote }}/{domain}.csr
-o {{ acme_certs_dir | quote }}/{domain}.d
-c {{ acme_challenge_dir | quote }}/.well-known/acme-challenge
minute: "30"
hour: "2"
state: present
loop: "{{ acme_domains | unique }}"
loop_control:
loop_var: domain_name
tags: acme_renew

View File

@ -0,0 +1,91 @@
- name: Create {{ domain_name }} certificates directory
file:
path: "{{ acme_certs_dir }}/{{ domain_name }}.d"
state: directory
owner: root
group: "{{ acme_ssl_group }}"
mode: "755"
tags: acme_install
- name: Generate Let's Encrypt account key
openssl_privatekey:
path: "{{ acme_accounts_dir }}/{{ acme_account_key }}"
owner: root
group: root
mode: "600"
type: RSA
size: 4096
tags: acme_account
- name: Generate Let's Encrypt private key for {{ domain_name }}
openssl_privatekey:
path: "{{ acme_keys_dir }}/{{ domain_name }}.pem"
owner: root
group: "{{ acme_ssl_group }}"
mode: "640"
type: RSA
size: 4096
- name: Generate Let's Encrypt CSR for {{ domain_name }}
openssl_csr:
path: "{{ acme_csr_dir }}/{{ domain_name }}.csr"
owner: root
group: "{{ acme_ssl_group }}"
mode: "644"
privatekey_path: "{{ acme_keys_dir }}/{{ domain_name }}.pem"
common_name: "{{ domain_name }}"
# - name: Check if Let's Encrypt certificate already exists for {{ domain_name }}
# stat:
# path: "{{ acme_certs_dir }}/{{ domain_name }}.d/cert.pem"
# register: _acme_cert_file
# - name: Check Let's Encrypt certificate expiration date for {{ domain_name }}
# openssl_certificate_info:
# path: "{{ acme_certs_dir }}/{{ domain_name }}.d/cert.pem"
# valid_at:
# thirty_days: "+30d"
# register: _acme_cert_validity
# when: _acme_cert_file.stat.isreg is defined and _acme_cert_file.stat.isreg
- name: Begin Let's Encrypt challenges for {{ domain_name }}
acme_certificate:
acme_directory: "{{ acme_directory }}"
acme_version: "{{ acme_version }}"
account_key_src: "{{ acme_accounts_dir }}/{{ acme_account_key }}"
account_email: "{{ acme_email }}"
terms_agreed: yes
challenge: http-01
csr: "{{ acme_csr_dir }}/{{ domain_name }}.csr"
dest: "{{ acme_certs_dir }}/{{ domain_name }}.d/cert.pem"
fullchain_dest: "{{ acme_certs_dir }}/{{ domain_name }}.d/fullchain.pem"
remaining_days: 30
register: _acme_challenge
# when: _acme_cert_validity is skipped or not _acme_cert_validity.valid_at.thirty_days
- debug:
var: _acme_challenge
# - name: Implement and complete Let's Encrypt challenge for {{ domain_name }}
# when: _acme_challenge is not skipped
# block:
# - name: Implement http-01 challenge files for {{ domain_name }}
# copy:
# content: "{{ _acme_challenge.challenge_data[domain_name]['http-01'].resource_value }}"
# dest: "{{ acme_challenge_dir }}/{{ _acme_challenge.challenge_data[domain_name]['http-01'].resource }}"
# owner: root
# group: root
# mode: "644"
# - name: Complete Let's Encrypt challenges for {{ domain_name }}
# acme_certificate:
# acme_directory: "{{ acme_directory }}"
# acme_version: "{{ acme_version }}"
# account_key_src: "{{ acme_accounts_dir }}/{{ acme_account_key }}"
# account_email: "{{ acme_email }}"
# challenge: http-01
# csr: "{{ acme_csr_dir }}/{{ domain_name }}.csr"
# dest: "{{ acme_certs_dir }}/{{ domain_name }}.d/cert.pem"
# chain_dest: "{{ acme_certs_dir }}/{{ domain_name }}.d/chain.pem"
# fullchain_dest: "{{ acme_certs_dir }}/{{ domain_name }}.d/fullchain.pem"
# data: "{{ _acme_challenge }}"

View File

@ -0,0 +1,2 @@
- import_tasks: acme.yml
tags: acme

View File

@ -0,0 +1 @@
acme_version: 2

View File

@ -0,0 +1,38 @@
Role Name
=========
A brief description of the role goes here.
Requirements
------------
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
Role Variables
--------------
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
Dependencies
------------
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
Example Playbook
----------------
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
- hosts: servers
roles:
- { role: username.rolename, x: 42 }
License
-------
BSD
Author Information
------------------
An optional section for the role authors to include contact information, or a website (HTML is not allowed).

View File

@ -0,0 +1,11 @@
---
# defaults file for common
node_exporter_path: /
node_exporter_port: 9100
node_exporter_public_port: "9180"
nginx_config_dir: /etc/nginx/conf.d
nginx_ssl_dir: /etc/nginx/ssl
ssh_config_dir: ~/.ssh
backup_targets: []

View File

@ -0,0 +1,8 @@
- name: restart openssh
service:
name: ssh
state: restarted
- name: reload nginx
include_tasks: ../handlers/nginx.yml
when: nginx_started is not changed

View File

@ -0,0 +1,7 @@
- name: Validate Nginx config
command: nginx -t
- name: Reload Nginx server
service:
name: nginx
state: reloaded

View File

@ -0,0 +1,16 @@
- import_tasks: repos.yml
tags: repos
- import_tasks: users.yml
tags: users
- import_tasks: openssh.yml
tags: openssh
- import_tasks: ufw.yml
tags: firewall
- import_tasks: msmtp.yml
tags: smtp
- import_tasks: nginx.yml
tags: nginx
- import_tasks: node_exporter.yml
tags: node_exporter
#- import_tasks: backup.yml
# tags: backup

View File

@ -0,0 +1,18 @@
---
# Install and configure SMTP relay
- name: Install msmtp
apt:
name:
- msmtp
- msmtp-mta
state: present
- name: Copy msmtp configuration
template:
src: msmtp/msmtprc.j2
dest: /etc/msmtprc
- name: Copy aliases
template:
src: msmtp/aliases.j2
dest: /etc/aliases

View File

@ -0,0 +1,101 @@
---
# Install and configure Nginx
- name: Install htpasswd dependencies
apt:
name: python-passlib
state: present
- name: Install SSL dependencies
apt:
name: ssl-cert
state: present
- name: Install Nginx
apt:
name: nginx-full
state: present
- name: Create Nginx configuration directories
file:
path: "{{ config_dir }}"
state: directory
owner: root
group: www-data
mode: "755"
loop:
- "{{ nginx_config_dir }}"
- "{{ nginx_ssl_dir }}"
loop_control:
loop_var: config_dir
- name: Generate Diffie-Hellman parameters
# This can take a long time... So we are doing it in async mode
openssl_dhparam:
path: "{{ nginx_ssl_dir }}/dhparam.pem"
size: 3072
owner: root
group: www-data
async: 3600
poll: 0
register: nginx_dh
- name: Use snakoil cert key as Nginx's default private key
file:
src: "/etc/ssl/private/ssl-cert-snakeoil.key"
path: "{{ nginx_ssl_dir }}/nginx.key"
state: link
owner: root
group: www-data
mode: "750"
force: yes
- name: Use snakoil cert as Nginx's default certificate
file:
src: "/etc/ssl/certs/ssl-cert-snakeoil.pem"
path: "{{ nginx_ssl_dir }}/nginx.crt"
state: link
owner: root
group: www-data
mode: "755"
force: yes
- name: Copy default Nginx config
template:
src: nginx/default.conf.j2
dest: /etc/nginx/sites-available/default
owner: root
group: www-data
mode: "755"
notify: reload nginx
- name: Enable default Nginx config
file:
src: /etc/nginx/sites-available/default
dest: /etc/nginx/sites-enabled/default
owner: root
group: www-data
mode: "755"
notify: reload nginx
- name: Allow default Nginx ports
ufw:
rule: allow
name: "Nginx Full"
- name: Waiting for Diffie-Hellman task to complete…
async_status:
jid: "{{ nginx_dh.ansible_job_id }}"
register: nginx_dh_job
retries: 60
delay: 30 # will retry every 30s for 30min (60 retries)
until: nginx_dh_job.finished
- name: Start Nginx server
service:
name: nginx
state: started
enabled: yes
register: nginx_started
- name: "Trigger Nginx handlers"
meta: flush_handlers

View File

@ -0,0 +1,30 @@
---
# Install and configure node-exporter
- name: Include role for installing node-exporter
include_role:
name: cloudalchemy.node-exporter
public: yes
vars:
node_exporter_web_listen_address: "0.0.0.0:{{ node_exporter_port }}"
- name: Configure Nginx for node-exporter
import_role:
name: nginx
vars:
nginx_config_file: node-exporter.conf
nginx_server:
name: "{{ inventory_hostname }}"
port: "{{ node_exporter_public_port }}"
locations:
- path: "{{ node_exporter_path }}"
basic_auth:
file: .htpasswd.node-exporter
password: "{{ node_exporter_password }}"
proxy_pass:
port: "{{ node_exporter_port }}"
path: /metrics
- name: Allow node-exporter port {{ node_exporter_public_port }}
ufw:
rule: allow
port: "{{ node_exporter_public_port }}"

View File

@ -0,0 +1,19 @@
---
# Configure OpenSSH server
- name: Configure OpenSSH server
template:
src: openssh/sshd_config.j2
dest: /etc/ssh/sshd_config
backup: yes
owner: "0"
group: "0"
mode: "0644"
validate: '/usr/sbin/sshd -T -f %s'
notify: restart openssh
- name: Trigger Ansible handlers
meta: flush_handlers
- name: Change Ansible SSH port to {{ openssh_port }}
set_fact:
ansible_port: "{{ openssh_port }}"

View File

@ -0,0 +1,12 @@
---
# Configure APT repositories and automatic upgrades
- name: Safely upgrade the server
apt:
upgrade: safe
update_cache: yes
cache_valid_time: "3600"
- name: Install unattend-upgrades for automatic upgrades
apt:
name: unattended-upgrades
state: present

View File

@ -0,0 +1,15 @@
---
# Install and configure UFW, the uncomplicated firewall
- name: Install UFW, the uncomplicated firewall
apt:
name: ufw
state: present
- name: Allow OpenSSH port {{ openssh_port }}
ufw:
rule: allow
port: "{{ openssh_port }}"
- name: Enable UFW config
ufw:
state: enabled

View File

@ -0,0 +1,22 @@
---
# Create an user and add their SSH public keys
- name: Create user {{ user.name }} with no password
user:
name: "{{ user.name }}"
shell: /bin/bash
# See https://unix.stackexchange.com/questions/193066/how-to-unlock-account-for-public-key-ssh-authorization-but-not-for-password-aut/193131#193131
password: '*'
groups:
- sudo
append: yes
state: present
update_password: on_create
- name: Add SSH public keys for user {{ user.name }}
authorized_key:
user: "{{ user.name }}"
state: present
# we can pass multiple SSH keys, but they must be separated by newlines
key: "{{ user.ssh_keys | join('\n') }}"
# remove obsolete keys
exclusive: yes

View File

@ -0,0 +1,28 @@
---
# Create users and add their SSH public keys
- name: Install sudo package
package:
name: sudo
state: present
tags: sudo
- name: Remove password to become root with sudo
lineinfile:
path: /etc/sudoers
state: present
regexp: '^%sudo'
line: '%sudo ALL=(ALL) NOPASSWD: ALL'
validate: 'visudo -cf %s'
tags: sudo
- name: Remove password for root user
user:
name: root
shell: /bin/bash
state: present
- name: Create users and add their SSH public keys
include_tasks: user.yml
loop: "{{ users }}"
loop_control:
loop_var: user

View File

@ -0,0 +1 @@
default: {{ smtp_default_contact }}

View File

@ -0,0 +1,19 @@
defaults
auth on
tls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile /var/log/msmtp.log
{% for account in smtp_accounts %}
account {{ account.name }}
host {{ account.host }}
port 587
from {{ account.from }}
user {{ account.user | default(account.from) }}
password {{ account.password }}
{% endfor %}
account default : {{ smtp_default_account }}
aliases /etc/aliases

View File

@ -0,0 +1,62 @@
{{ ansible_managed | comment }}
# Default server configuration
#
server {
listen 80 default_server;
listen [::]:80 default_server ipv6only=on;
root /var/www/default;
server_name _;
location ^~ /.well-known/acme-challenge/ {
allow all;
root /var/www/acme;
try_files $uri =404;
}
location / {
try_files $uri $uri/ =404;
}
location = /favicon.ico {
log_not_found off;
access_log off;
}
location = /robots.txt {
allow all;
log_not_found off;
access_log off;
}
location ~* ^.+.(jpg|jpeg|gif|png|ico|css|zip|tgz|gz|rar|bz2|doc|xls|exe|pdf|ppt|txt|tar|mid|midi|wav|bmp|rtf|js)$ {
expires 7d;
log_not_found off;
access_log off;
}
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
location ~ /nginx_status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}
}
server {
listen 443 default_server;
listen [::]:443 default_server;
server_name _;
include snippets/snakeoil.conf;
return 444;
}

View File

@ -0,0 +1,31 @@
{{ ansible_managed | comment }}
{% if nginx_server_name is defined %}
server {
listen {{ nginx_port }};
server_name {{ nginx_server_name }};
{% endif %}
{% for location in nginx_locations %}
location {{ location }} {
{% if location.proxy_pass is defined %}
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
{% endif %}
{% if location.basic_auth_file is defined %}
auth_basic "Authentication required";
auth_basic_user_file /etc/nginx/{{ location.basic_auth_file }};
{% endif %}
{% if location.proxy_pass is defined %}
proxy_pass http://localhost:{{ location.proxy_pass.port | default('80') }}{{ location.proxy_pass.path }};
{% endif %}
}
{% endif %}
{% if nginx_server_name is defined %}
}
{% endif %}

View File

@ -0,0 +1,17 @@
# {{ ansible_managed }}
Port {{ openssh_port }}
PermitRootLogin no
ChallengeResponseAuthentication no
UsePAM yes
X11Forwarding yes
PrintMotd no
AcceptEnv LANG LC_*
Subsystem sftp /usr/lib/openssh/sftp-server

View File

@ -0,0 +1,2 @@
---
# vars file for common

View File

@ -0,0 +1,38 @@
Role Name
=========
A brief description of the role goes here.
Requirements
------------
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
Role Variables
--------------
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
Dependencies
------------
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
Example Playbook
----------------
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
- hosts: servers
roles:
- { role: username.rolename, x: 42 }
License
-------
BSD
Author Information
------------------
An optional section for the role authors to include contact information, or a website (HTML is not allowed).

View File

@ -0,0 +1,2 @@
---
# defaults file for roles/docker

View File

@ -0,0 +1,2 @@
---
# handlers file for roles/docker

View File

@ -0,0 +1,53 @@
galaxy_info:
author: your name
description: your description
company: your company (optional)
# If the issue tracker for your role is not on github, uncomment the
# next line and provide a value
# issue_tracker_url: http://example.com/issue/tracker
# Choose a valid license ID from https://spdx.org - some suggested licenses:
# - BSD-3-Clause (default)
# - MIT
# - GPL-2.0-or-later
# - GPL-3.0-only
# - Apache-2.0
# - CC-BY-4.0
license: license (GPL-2.0-or-later, MIT, etc)
min_ansible_version: 2.4
# If this a Container Enabled role, provide the minimum Ansible Container version.
# min_ansible_container_version:
#
# Provide a list of supported platforms, and for each platform a list of versions.
# If you don't wish to enumerate all versions for a particular platform, use 'all'.
# To view available platforms and versions (or releases), visit:
# https://galaxy.ansible.com/api/v1/platforms/
#
# platforms:
# - name: Fedora
# versions:
# - all
# - 25
# - name: SomePlatform
# versions:
# - all
# - 1.0
# - 7
# - 99.99
galaxy_tags: []
# List tags for your role here, one per line. A tag is a keyword that describes
# and categorizes the role. Users find roles by searching for tags. Be sure to
# remove the '[]' above, if you add tags to this list.
#
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
# Maximum 20 tags per role.
dependencies: []
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
# if you add dependencies to this list.

View File

@ -0,0 +1,45 @@
---
# tasks file for roles/docker
- name: install required system packages
apt:
names:
- apt-transport-https
- 'ca-certificates'
- 'curl'
- 'software-properties-common'
- 'python3-pip'
- 'virtualenv'
- 'python3-setuptools'
state: present
- name: add Docker GPG apt Key
apt_key:
url: https://download.docker.com/linux/ubuntu/gpg
state: present
- name: add Docker Repository
apt_repository:
repo: deb https://download.docker.com/linux/ubuntu bionic stable
state: present
register: docker_repo
- name: update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
when: docker_repo is changed
- name: install docker-ce package
apt:
name: docker-ce
state: present
- name: enable docker service
systemd:
name: docker
state: started
enabled: yes
- name: install Docker Module for Python
pip:
name: docker

View File

@ -0,0 +1,2 @@
localhost

View File

@ -0,0 +1,5 @@
---
- hosts: localhost
remote_user: root
roles:
- roles/docker

View File

@ -0,0 +1,2 @@
---
# vars file for roles/docker

View File

@ -0,0 +1,17 @@
---
# defaults file for roles/prometheus
grafana_admin_user: admin
grafana_domain: "{{ inventory_hostname }}"
grafana_web_path: /grafana
grafana_protocol: http
grafana_port: 3000
prometheus_domain: "{{ inventory_hostname }}"
prometheus_web_path: /prometheus
prometheus_port: 9090
nginx_default_path: "{{ grafana_web_path }}"
alertmanager_domain: "{{ inventory_hostname }}"
alertmanager_web_path: /alertmanager
alertmanager_port: 9093

View File

@ -0,0 +1,43 @@
groups:
- name: Hardware alerts
rules:
- alert: Instancedown
expr: up == 0
for: 5m
labels:
severity: warning
annotations:
title: Instance {{ $labels.instance }} is down
description: Failed to scrape {{ $labels.job }} on {{ $labels.instance }} for more than 5 minute. Instance seems down.
- alert: LowRootDiskSpace
expr: (node_filesystem_free_bytes{device =~ "/dev/.+"} / node_filesystem_size_bytes{device =~ "/dev/.+"} * 100) < 10
for: 5m
labels:
severity: warning
annotations:
title: Low free root space on {{ $labels.instance }}
description: On {{ $labels.instance }} device {{ $labels.device }} mounted on {{ $labels.mountpoint }} has low free space of {{ $value }}%
- alert: LowDataDiskSpace
expr: (node_filesystem_free_bytes{device !~ "/dev/.+", fstype !~ "tmpfs|.*lxcfs"} / node_filesystem_size_bytes{device !~ "/dev/.+", fstype !~ "tmpfs|.*lxcfs"} * 100) < 10
for: 5m
labels:
severity: warning
annotations:
title: Low free data space on {{ $labels.instance }}
description: On {{ $labels.instance }} device {{ $labels.device }} mounted on {{ $labels.mountpoint }} has low free space of {{ $value }}%
- alert: HighCPULoad
expr: 100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 90
for: 5m
labels:
severity: warning
annotations:
title: High CPU load on instance {{ $labels.instance }}
description: Instance {{ $labels.instance }} has high CPU load.
- alert: HighMemoryUsage
expr: (1 - ((node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes) / node_memory_MemTotal_bytes)) * 100 > 90
for: 5m
labels:
severity: warning
annotations:
description: Instance {{ $labels.instance }} has high memory usage
summary: High memory usage on {{ $labels.instance }}

View File

@ -0,0 +1,13 @@
---
# handlers file for roles/prometheus
- name: restart grafana
service:
name: grafana-server
state: restarted
when: not grafana_service.changed
- name: restart prometheus
service:
name: prometheus
state: restarted
when: not prometheus_service.changed

View File

@ -0,0 +1,24 @@
- name: Install Alertmanager
import_role:
name: cloudalchemy.alertmanager
public: yes
vars:
alertmanager_web_external_url: "http://{{ alertmanager_domain }}{{ alertmanager_web_path }}"
alertmanager_config_flags_extra:
web.route-prefix: /
alertmanager_web_listen_address: "0.0.0.0:{{ alertmanager_port }}"
- name: Configure Nginx for Alertmaneger
import_role:
name: nginx
vars:
nginx_config_file: "{{ inventory_hostname }}.d/alertmanager.conf"
nginx_server:
locations:
- path: "{{ alertmanager_web_path }}/"
basic_auth:
file: .htpasswd.alertmanager
password: "{{ alertmanager_password }}"
proxy_pass:
port: "{{ alertmanager_port }}"
path: /

View File

@ -0,0 +1,71 @@
- name: add grafana GPG apt key
apt_key:
url: https://packages.grafana.com/gpg.key
state: present
- block:
- name: add grafana repository
apt_repository:
repo: deb https://packages.grafana.com/oss/deb stable main
state: present
register: grafana_repo
notify: restart grafana
- name: update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
when: grafana_repo is changed
- name: install grafana package
apt:
pkg: grafana
state: present
notify: restart grafana
- name: copy grafana config
template:
src: grafana.ini.j2
dest: /etc/grafana/grafana.ini
become: yes
notify: restart grafana
- name: enable grafana service
systemd:
name: grafana-server
state: started
enabled: yes
register: grafana_service
- name: Configure Nginx for Grafana
import_role:
name: nginx
vars:
nginx_config_file: "{{ inventory_hostname }}.conf"
nginx_server:
name: "{{ inventory_hostname }}"
port: 80
locations:
- path: "= /"
return:
code: 301
url: "http://{{ inventory_hostname }}{{ grafana_web_path }}"
includes:
- "{{ nginx_config_dir }}/{{ inventory_hostname }}.d/*.conf"
- name: ensure nginx config directory exists
file:
path: "{{ nginx_config_dir }}/{{ inventory_hostname }}.d"
state: directory
- include_role:
name: nginx
tasks_from: configure
vars:
nginx_config_file: "{{ inventory_hostname }}.d/grafana.conf"
nginx_server:
locations:
- path: "{{ grafana_web_path }}/"
proxy_pass:
port: "{{ grafana_port }}"
path: /

View File

@ -0,0 +1,8 @@
---
- import_tasks: grafana.yml
become: yes
tags: grafana
- import_tasks: alertmanager.yml
tags: alertmanager
- import_tasks: prometheus.yml
tags: prometheus

View File

@ -0,0 +1,26 @@
- name: Install Prometheus
import_role:
name: cloudalchemy.prometheus
public: yes
vars:
prometheus_config_file: "prometheus_custom.yml.j2"
prometheus_alert_rules: []
prometheus_web_external_url: "http://{{ prometheus_domain }}{{ prometheus_web_path }}"
prometheus_config_flags_extra:
web.route-prefix: /
prometheus_web_listen_address: "0.0.0.0:{{ prometheus_port }}"
- name: Configure Nginx for Prometheus
import_role:
name: nginx
vars:
nginx_config_file: "{{ inventory_hostname }}.d/prometheus.conf"
nginx_server:
locations:
- path: "{{ prometheus_web_path }}/"
basic_auth:
file: .htpasswd.prometheus
password: "{{ prometheus_password }}"
proxy_pass:
port: "{{ prometheus_port }}"
path: /

View File

@ -0,0 +1,627 @@
[server]
[security]
##################### Grafana Configuration Example #####################
#
# Everything has defaults so you only need to uncomment things you want to
# change
# possible values : production, development
;app_mode = production
# instance name, defaults to HOSTNAME environment variable value or hostname if HOSTNAME var is empty
;instance_name = ${HOSTNAME}
#################################### Paths ####################################
[paths]
# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used)
;data = /var/lib/grafana
# Temporary files in `data` directory older than given duration will be removed
;temp_data_lifetime = 24h