From 961498e32b4ca8c688150d5e25d5ce8999241b09 Mon Sep 17 00:00:00 2001 From: HgO Date: Mon, 13 Apr 2020 14:46:45 +0200 Subject: [PATCH] initial commit --- .gitignore | 1 + inventories/group_vars/all.yml | 45 ++ inventories/group_vars/monitoring.yml | 41 ++ inventories/group_vars/mumble.yml | 46 ++ inventories/group_vars/yunohost.yml | 1 + .../host_vars/status.pirateparty.be.yml | 8 + inventories/hosts.ini | 11 + playbooks/common.yml | 12 + playbooks/monitoring.yml | 4 + playbooks/mumble.yml | 10 + roles/acme/defaults/main.yml | 9 + roles/acme/files/acme_renew_cert.py | 184 +++++ roles/acme/tasks/acme.yml | 76 +++ roles/acme/tasks/challenge.yml | 91 +++ roles/acme/tasks/main.yml | 2 + roles/acme/vars/main.yml | 1 + roles/common/README.md | 38 ++ roles/common/defaults/main.yml | 11 + roles/common/handlers/main.yml | 8 + roles/common/handlers/nginx.yml | 7 + roles/common/tasks/main.yml | 16 + roles/common/tasks/msmtp.yml | 18 + roles/common/tasks/nginx.yml | 101 +++ roles/common/tasks/node_exporter.yml | 30 + roles/common/tasks/openssh.yml | 19 + roles/common/tasks/repos.yml | 12 + roles/common/tasks/ufw.yml | 15 + roles/common/tasks/user.yml | 22 + roles/common/tasks/users.yml | 28 + roles/common/templates/msmtp/aliases.j2 | 1 + roles/common/templates/msmtp/msmtprc.j2 | 19 + roles/common/templates/nginx/default.conf.j2 | 62 ++ .../templates/nginx/node-exporter.conf.j2 | 31 + roles/common/templates/openssh/sshd_config.j2 | 17 + roles/common/vars/main.yml | 2 + roles/docker/README.md | 38 ++ roles/docker/defaults/main.yml | 2 + roles/docker/handlers/main.yml | 2 + roles/docker/meta/main.yml | 53 ++ roles/docker/tasks/main.yml | 45 ++ roles/docker/tests/inventory | 2 + roles/docker/tests/test.yml | 5 + roles/docker/vars/main.yml | 2 + roles/monitoring/defaults/main.yml | 17 + .../files/prometheus/rules/hardware.rules | 43 ++ roles/monitoring/handlers/main.yml | 13 + roles/monitoring/tasks/alertmanager.yml | 24 + roles/monitoring/tasks/grafana.yml | 71 ++ roles/monitoring/tasks/main.yml | 8 + roles/monitoring/tasks/prometheus.yml | 26 + roles/monitoring/templates/grafana.ini.j2 | 627 ++++++++++++++++++ .../templates/nginx/alertmanager.conf.j2 | 11 + .../templates/nginx/grafana.conf.j2 | 5 + .../templates/nginx/prometheus.conf.j2 | 11 + roles/monitoring/templates/prometheus.env.j2 | 60 ++ roles/monitoring/templates/prometheus.yml.j2 | 31 + .../templates/prometheus_custom.yml.j2 | 40 ++ roles/mumble/defaults/main.yml | 24 + roles/mumble/handlers/main.yml | 21 + roles/mumble/handlers/nginx.yml | 7 + roles/mumble/meta/main.yml | 5 + roles/mumble/tasks/main.yml | 11 + roles/mumble/tasks/mumble_web.yml | 77 +++ roles/mumble/tasks/nginx.yml | 18 + roles/mumble/tasks/umurmur.yml | 88 +++ roles/mumble/templates/mumble-web.js.j2 | 23 + roles/mumble/templates/mumble-web.service.j2 | 26 + roles/mumble/templates/nginx.conf.j2 | 65 ++ roles/mumble/templates/umurmur.conf.j2 | 72 ++ roles/mumble/templates/umurmur.service.j2 | 28 + roles/nginx/README.md | 38 ++ roles/nginx/defaults/main.yml | 5 + roles/nginx/tasks/configure.yml | 28 + roles/nginx/tasks/main.yml | 4 + roles/nginx/templates/nginx.conf.j2 | 38 ++ roles/nginx/vars/main.yml | 2 + 76 files changed, 2715 insertions(+) create mode 100644 .gitignore create mode 100644 inventories/group_vars/all.yml create mode 100644 inventories/group_vars/monitoring.yml create mode 100644 inventories/group_vars/mumble.yml create mode 100644 inventories/group_vars/yunohost.yml create mode 100644 inventories/host_vars/status.pirateparty.be.yml create mode 100644 inventories/hosts.ini create mode 100644 playbooks/common.yml create mode 100644 playbooks/monitoring.yml create mode 100644 playbooks/mumble.yml create mode 100644 roles/acme/defaults/main.yml create mode 100755 roles/acme/files/acme_renew_cert.py create mode 100644 roles/acme/tasks/acme.yml create mode 100644 roles/acme/tasks/challenge.yml create mode 100644 roles/acme/tasks/main.yml create mode 100644 roles/acme/vars/main.yml create mode 100644 roles/common/README.md create mode 100644 roles/common/defaults/main.yml create mode 100644 roles/common/handlers/main.yml create mode 100644 roles/common/handlers/nginx.yml create mode 100644 roles/common/tasks/main.yml create mode 100644 roles/common/tasks/msmtp.yml create mode 100644 roles/common/tasks/nginx.yml create mode 100644 roles/common/tasks/node_exporter.yml create mode 100644 roles/common/tasks/openssh.yml create mode 100644 roles/common/tasks/repos.yml create mode 100644 roles/common/tasks/ufw.yml create mode 100644 roles/common/tasks/user.yml create mode 100644 roles/common/tasks/users.yml create mode 100644 roles/common/templates/msmtp/aliases.j2 create mode 100644 roles/common/templates/msmtp/msmtprc.j2 create mode 100644 roles/common/templates/nginx/default.conf.j2 create mode 100644 roles/common/templates/nginx/node-exporter.conf.j2 create mode 100644 roles/common/templates/openssh/sshd_config.j2 create mode 100644 roles/common/vars/main.yml create mode 100644 roles/docker/README.md create mode 100644 roles/docker/defaults/main.yml create mode 100644 roles/docker/handlers/main.yml create mode 100644 roles/docker/meta/main.yml create mode 100644 roles/docker/tasks/main.yml create mode 100644 roles/docker/tests/inventory create mode 100644 roles/docker/tests/test.yml create mode 100644 roles/docker/vars/main.yml create mode 100644 roles/monitoring/defaults/main.yml create mode 100644 roles/monitoring/files/prometheus/rules/hardware.rules create mode 100644 roles/monitoring/handlers/main.yml create mode 100644 roles/monitoring/tasks/alertmanager.yml create mode 100644 roles/monitoring/tasks/grafana.yml create mode 100644 roles/monitoring/tasks/main.yml create mode 100644 roles/monitoring/tasks/prometheus.yml create mode 100644 roles/monitoring/templates/grafana.ini.j2 create mode 100644 roles/monitoring/templates/nginx/alertmanager.conf.j2 create mode 100644 roles/monitoring/templates/nginx/grafana.conf.j2 create mode 100644 roles/monitoring/templates/nginx/prometheus.conf.j2 create mode 100644 roles/monitoring/templates/prometheus.env.j2 create mode 100644 roles/monitoring/templates/prometheus.yml.j2 create mode 100644 roles/monitoring/templates/prometheus_custom.yml.j2 create mode 100644 roles/mumble/defaults/main.yml create mode 100644 roles/mumble/handlers/main.yml create mode 100644 roles/mumble/handlers/nginx.yml create mode 100644 roles/mumble/meta/main.yml create mode 100644 roles/mumble/tasks/main.yml create mode 100644 roles/mumble/tasks/mumble_web.yml create mode 100644 roles/mumble/tasks/nginx.yml create mode 100644 roles/mumble/tasks/umurmur.yml create mode 100644 roles/mumble/templates/mumble-web.js.j2 create mode 100644 roles/mumble/templates/mumble-web.service.j2 create mode 100644 roles/mumble/templates/nginx.conf.j2 create mode 100644 roles/mumble/templates/umurmur.conf.j2 create mode 100644 roles/mumble/templates/umurmur.service.j2 create mode 100644 roles/nginx/README.md create mode 100644 roles/nginx/defaults/main.yml create mode 100644 roles/nginx/tasks/configure.yml create mode 100644 roles/nginx/tasks/main.yml create mode 100644 roles/nginx/templates/nginx.conf.j2 create mode 100644 roles/nginx/vars/main.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..600d2d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode \ No newline at end of file diff --git a/inventories/group_vars/all.yml b/inventories/group_vars/all.yml new file mode 100644 index 0000000..b5eb521 --- /dev/null +++ b/inventories/group_vars/all.yml @@ -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 \ No newline at end of file diff --git a/inventories/group_vars/monitoring.yml b/inventories/group_vars/monitoring.yml new file mode 100644 index 0000000..d6669be --- /dev/null +++ b/inventories/group_vars/monitoring.yml @@ -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 diff --git a/inventories/group_vars/mumble.yml b/inventories/group_vars/mumble.yml new file mode 100644 index 0000000..de08213 --- /dev/null +++ b/inventories/group_vars/mumble.yml @@ -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 \ No newline at end of file diff --git a/inventories/group_vars/yunohost.yml b/inventories/group_vars/yunohost.yml new file mode 100644 index 0000000..073906f --- /dev/null +++ b/inventories/group_vars/yunohost.yml @@ -0,0 +1 @@ +ansible_user: admin diff --git a/inventories/host_vars/status.pirateparty.be.yml b/inventories/host_vars/status.pirateparty.be.yml new file mode 100644 index 0000000..aef36db --- /dev/null +++ b/inventories/host_vars/status.pirateparty.be.yml @@ -0,0 +1,8 @@ +storage_box_username: u212275-sub5 +storage_box_password: !vault | + $ANSIBLE_VAULT;1.1;AES256 + 36313662333062323531613966386365373339663566303133653562663838316632613830613264 + 6564333736343830623061313534313630313534316231390a666662633861383563333562356561 + 64616534313266323833383331313334333761323965383634666635663430366461353437616465 + 6337363536643738310a656530663837386537336434633037376463336165613239323265366234 + 64663863333763356430616635323061396663373264343666323831646664646430 diff --git a/inventories/hosts.ini b/inventories/hosts.ini new file mode 100644 index 0000000..d9827cc --- /dev/null +++ b/inventories/hosts.ini @@ -0,0 +1,11 @@ +[web] +mastodon.pirateparty.be +pirateparty.be +wiki.pirateparty.be +talk.parley.be + +[monitoring] +status.pirateparty.be + +[mumble] +talk.parley.be \ No newline at end of file diff --git a/playbooks/common.yml b/playbooks/common.yml new file mode 100644 index 0000000..9a3def3 --- /dev/null +++ b/playbooks/common.yml @@ -0,0 +1,12 @@ +--- +- hosts: all + become: yes + + pre_tasks: + - name: Update apt cache + apt: + update_cache: yes + cache_valid_time: 3600 + + roles: + - common diff --git a/playbooks/monitoring.yml b/playbooks/monitoring.yml new file mode 100644 index 0000000..b003b0e --- /dev/null +++ b/playbooks/monitoring.yml @@ -0,0 +1,4 @@ +- hosts: monitoring + roles: + - common + - monitoring diff --git a/playbooks/mumble.yml b/playbooks/mumble.yml new file mode 100644 index 0000000..9751694 --- /dev/null +++ b/playbooks/mumble.yml @@ -0,0 +1,10 @@ +- hosts: mumble + become: yes + + vars: + acme_domains: + - "{{ umurmur_domain }}" + - "{{ mumble_web_domain }}" + roles: + - acme + - mumble \ No newline at end of file diff --git a/roles/acme/defaults/main.yml b/roles/acme/defaults/main.yml new file mode 100644 index 0000000..d40b76e --- /dev/null +++ b/roles/acme/defaults/main.yml @@ -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 \ No newline at end of file diff --git a/roles/acme/files/acme_renew_cert.py b/roles/acme/files/acme_renew_cert.py new file mode 100755 index 0000000..4e0bb09 --- /dev/null +++ b/roles/acme/files/acme_renew_cert.py @@ -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) \ No newline at end of file diff --git a/roles/acme/tasks/acme.yml b/roles/acme/tasks/acme.yml new file mode 100644 index 0000000..1f1b137 --- /dev/null +++ b/roles/acme/tasks/acme.yml @@ -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 \ No newline at end of file diff --git a/roles/acme/tasks/challenge.yml b/roles/acme/tasks/challenge.yml new file mode 100644 index 0000000..7b56783 --- /dev/null +++ b/roles/acme/tasks/challenge.yml @@ -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 }}" \ No newline at end of file diff --git a/roles/acme/tasks/main.yml b/roles/acme/tasks/main.yml new file mode 100644 index 0000000..2b94819 --- /dev/null +++ b/roles/acme/tasks/main.yml @@ -0,0 +1,2 @@ +- import_tasks: acme.yml + tags: acme \ No newline at end of file diff --git a/roles/acme/vars/main.yml b/roles/acme/vars/main.yml new file mode 100644 index 0000000..8fb9bcb --- /dev/null +++ b/roles/acme/vars/main.yml @@ -0,0 +1 @@ +acme_version: 2 \ No newline at end of file diff --git a/roles/common/README.md b/roles/common/README.md new file mode 100644 index 0000000..225dd44 --- /dev/null +++ b/roles/common/README.md @@ -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). diff --git a/roles/common/defaults/main.yml b/roles/common/defaults/main.yml new file mode 100644 index 0000000..0cedb9c --- /dev/null +++ b/roles/common/defaults/main.yml @@ -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: [] diff --git a/roles/common/handlers/main.yml b/roles/common/handlers/main.yml new file mode 100644 index 0000000..18ebed3 --- /dev/null +++ b/roles/common/handlers/main.yml @@ -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 \ No newline at end of file diff --git a/roles/common/handlers/nginx.yml b/roles/common/handlers/nginx.yml new file mode 100644 index 0000000..b8dc381 --- /dev/null +++ b/roles/common/handlers/nginx.yml @@ -0,0 +1,7 @@ +- name: Validate Nginx config + command: nginx -t + +- name: Reload Nginx server + service: + name: nginx + state: reloaded \ No newline at end of file diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml new file mode 100644 index 0000000..8c12c37 --- /dev/null +++ b/roles/common/tasks/main.yml @@ -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 diff --git a/roles/common/tasks/msmtp.yml b/roles/common/tasks/msmtp.yml new file mode 100644 index 0000000..1a2ecb1 --- /dev/null +++ b/roles/common/tasks/msmtp.yml @@ -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 \ No newline at end of file diff --git a/roles/common/tasks/nginx.yml b/roles/common/tasks/nginx.yml new file mode 100644 index 0000000..750f72d --- /dev/null +++ b/roles/common/tasks/nginx.yml @@ -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 \ No newline at end of file diff --git a/roles/common/tasks/node_exporter.yml b/roles/common/tasks/node_exporter.yml new file mode 100644 index 0000000..b647afb --- /dev/null +++ b/roles/common/tasks/node_exporter.yml @@ -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 }}" diff --git a/roles/common/tasks/openssh.yml b/roles/common/tasks/openssh.yml new file mode 100644 index 0000000..51257c3 --- /dev/null +++ b/roles/common/tasks/openssh.yml @@ -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 }}" diff --git a/roles/common/tasks/repos.yml b/roles/common/tasks/repos.yml new file mode 100644 index 0000000..42ad9c1 --- /dev/null +++ b/roles/common/tasks/repos.yml @@ -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 \ No newline at end of file diff --git a/roles/common/tasks/ufw.yml b/roles/common/tasks/ufw.yml new file mode 100644 index 0000000..975a376 --- /dev/null +++ b/roles/common/tasks/ufw.yml @@ -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 \ No newline at end of file diff --git a/roles/common/tasks/user.yml b/roles/common/tasks/user.yml new file mode 100644 index 0000000..5ba7306 --- /dev/null +++ b/roles/common/tasks/user.yml @@ -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 \ No newline at end of file diff --git a/roles/common/tasks/users.yml b/roles/common/tasks/users.yml new file mode 100644 index 0000000..82652b4 --- /dev/null +++ b/roles/common/tasks/users.yml @@ -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 \ No newline at end of file diff --git a/roles/common/templates/msmtp/aliases.j2 b/roles/common/templates/msmtp/aliases.j2 new file mode 100644 index 0000000..a7c983a --- /dev/null +++ b/roles/common/templates/msmtp/aliases.j2 @@ -0,0 +1 @@ +default: {{ smtp_default_contact }} \ No newline at end of file diff --git a/roles/common/templates/msmtp/msmtprc.j2 b/roles/common/templates/msmtp/msmtprc.j2 new file mode 100644 index 0000000..469810b --- /dev/null +++ b/roles/common/templates/msmtp/msmtprc.j2 @@ -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 diff --git a/roles/common/templates/nginx/default.conf.j2 b/roles/common/templates/nginx/default.conf.j2 new file mode 100644 index 0000000..d21273b --- /dev/null +++ b/roles/common/templates/nginx/default.conf.j2 @@ -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; +} \ No newline at end of file diff --git a/roles/common/templates/nginx/node-exporter.conf.j2 b/roles/common/templates/nginx/node-exporter.conf.j2 new file mode 100644 index 0000000..b6b48f9 --- /dev/null +++ b/roles/common/templates/nginx/node-exporter.conf.j2 @@ -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 %} diff --git a/roles/common/templates/openssh/sshd_config.j2 b/roles/common/templates/openssh/sshd_config.j2 new file mode 100644 index 0000000..ca70fb7 --- /dev/null +++ b/roles/common/templates/openssh/sshd_config.j2 @@ -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 diff --git a/roles/common/vars/main.yml b/roles/common/vars/main.yml new file mode 100644 index 0000000..fed6035 --- /dev/null +++ b/roles/common/vars/main.yml @@ -0,0 +1,2 @@ +--- +# vars file for common diff --git a/roles/docker/README.md b/roles/docker/README.md new file mode 100644 index 0000000..225dd44 --- /dev/null +++ b/roles/docker/README.md @@ -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). diff --git a/roles/docker/defaults/main.yml b/roles/docker/defaults/main.yml new file mode 100644 index 0000000..f28207e --- /dev/null +++ b/roles/docker/defaults/main.yml @@ -0,0 +1,2 @@ +--- +# defaults file for roles/docker \ No newline at end of file diff --git a/roles/docker/handlers/main.yml b/roles/docker/handlers/main.yml new file mode 100644 index 0000000..f9b9124 --- /dev/null +++ b/roles/docker/handlers/main.yml @@ -0,0 +1,2 @@ +--- +# handlers file for roles/docker \ No newline at end of file diff --git a/roles/docker/meta/main.yml b/roles/docker/meta/main.yml new file mode 100644 index 0000000..3a212a9 --- /dev/null +++ b/roles/docker/meta/main.yml @@ -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. + \ No newline at end of file diff --git a/roles/docker/tasks/main.yml b/roles/docker/tasks/main.yml new file mode 100644 index 0000000..9cc5577 --- /dev/null +++ b/roles/docker/tasks/main.yml @@ -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 diff --git a/roles/docker/tests/inventory b/roles/docker/tests/inventory new file mode 100644 index 0000000..878877b --- /dev/null +++ b/roles/docker/tests/inventory @@ -0,0 +1,2 @@ +localhost + diff --git a/roles/docker/tests/test.yml b/roles/docker/tests/test.yml new file mode 100644 index 0000000..8d40359 --- /dev/null +++ b/roles/docker/tests/test.yml @@ -0,0 +1,5 @@ +--- +- hosts: localhost + remote_user: root + roles: + - roles/docker \ No newline at end of file diff --git a/roles/docker/vars/main.yml b/roles/docker/vars/main.yml new file mode 100644 index 0000000..3520664 --- /dev/null +++ b/roles/docker/vars/main.yml @@ -0,0 +1,2 @@ +--- +# vars file for roles/docker \ No newline at end of file diff --git a/roles/monitoring/defaults/main.yml b/roles/monitoring/defaults/main.yml new file mode 100644 index 0000000..b40ef09 --- /dev/null +++ b/roles/monitoring/defaults/main.yml @@ -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 diff --git a/roles/monitoring/files/prometheus/rules/hardware.rules b/roles/monitoring/files/prometheus/rules/hardware.rules new file mode 100644 index 0000000..4545026 --- /dev/null +++ b/roles/monitoring/files/prometheus/rules/hardware.rules @@ -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 }} diff --git a/roles/monitoring/handlers/main.yml b/roles/monitoring/handlers/main.yml new file mode 100644 index 0000000..7e59484 --- /dev/null +++ b/roles/monitoring/handlers/main.yml @@ -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 diff --git a/roles/monitoring/tasks/alertmanager.yml b/roles/monitoring/tasks/alertmanager.yml new file mode 100644 index 0000000..fa594d3 --- /dev/null +++ b/roles/monitoring/tasks/alertmanager.yml @@ -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: / diff --git a/roles/monitoring/tasks/grafana.yml b/roles/monitoring/tasks/grafana.yml new file mode 100644 index 0000000..987d9fb --- /dev/null +++ b/roles/monitoring/tasks/grafana.yml @@ -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: / diff --git a/roles/monitoring/tasks/main.yml b/roles/monitoring/tasks/main.yml new file mode 100644 index 0000000..dde42a5 --- /dev/null +++ b/roles/monitoring/tasks/main.yml @@ -0,0 +1,8 @@ +--- +- import_tasks: grafana.yml + become: yes + tags: grafana +- import_tasks: alertmanager.yml + tags: alertmanager +- import_tasks: prometheus.yml + tags: prometheus \ No newline at end of file diff --git a/roles/monitoring/tasks/prometheus.yml b/roles/monitoring/tasks/prometheus.yml new file mode 100644 index 0000000..bff20d8 --- /dev/null +++ b/roles/monitoring/tasks/prometheus.yml @@ -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: / diff --git a/roles/monitoring/templates/grafana.ini.j2 b/roles/monitoring/templates/grafana.ini.j2 new file mode 100644 index 0000000..44cd9ef --- /dev/null +++ b/roles/monitoring/templates/grafana.ini.j2 @@ -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 + +# Directory where grafana can store logs +;logs = /var/log/grafana + +# Directory where grafana will automatically scan and look for plugins +;plugins = /var/lib/grafana/plugins + +# folder that contains provisioning config files that grafana will apply on startup and while running. +;provisioning = conf/provisioning + +#################################### Server #################################### +[server] +# Protocol (http, https, h2, socket) +protocol = {{ grafana_protocol }} + +# The ip address to bind to, empty will bind to all interfaces +;http_addr = + +# The http port to use +http_port = {{ grafana_port }} + +# The public facing domain name used to access grafana from a browser +domain = {{ grafana_domain }} + +# Redirect to correct domain if host header does not match domain +# Prevents DNS rebinding attacks +;enforce_domain = false + +# The full public facing url you use in browser, used for redirects and emails +# If you use reverse proxy and sub path specify full url (with sub path) +root_url = {{ grafana_protocol }}://{{ grafana_domain }}{{ grafana_web_path }} + +# Serve Grafana from subpath specified in `root_url` setting. By default it is set to `false` for compatibility reasons. +;serve_from_sub_path = false + +# Log web requests +;router_logging = false + +# the path relative working path +;static_root_path = public + +# enable gzip +;enable_gzip = false + +# https certs & key file +;cert_file = +;cert_key = + +# Unix socket path +;socket = + +#################################### Database #################################### +[database] +# You can configure the database connection by specifying type, host, name, user and password +# as separate properties or as on string using the url properties. + +# Either "mysql", "postgres" or "sqlite3", it's your choice +;type = sqlite3 +;host = 127.0.0.1:3306 +;name = grafana +;user = root +# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;""" +;password = + +# Use either URL or the previous fields to configure the database +# Example: mysql://user:secret@host:port/database +;url = + +# For "postgres" only, either "disable", "require" or "verify-full" +;ssl_mode = disable + +# For "sqlite3" only, path relative to data_path setting +;path = grafana.db + +# Max idle conn setting default is 2 +;max_idle_conn = 2 + +# Max conn setting default is 0 (mean not set) +;max_open_conn = + +# Connection Max Lifetime default is 14400 (means 14400 seconds or 4 hours) +;conn_max_lifetime = 14400 + +# Set to true to log the sql calls and execution times. +;log_queries = + +# For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared) +;cache_mode = private + +#################################### Cache server ############################# +[remote_cache] +# Either "redis", "memcached" or "database" default is "database" +;type = database + +# cache connectionstring options +# database: will use Grafana primary database. +# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=0,ssl=false`. Only addr is required. ssl may be 'true', 'false', or 'insecure'. +# memcache: 127.0.0.1:11211 +;connstr = + +#################################### Data proxy ########################### +[dataproxy] + +# This enables data proxy logging, default is false +;logging = false + +# How long the data proxy should wait before timing out default is 30 (seconds) +;timeout = 30 + +# If enabled and user is not anonymous, data proxy will add X-Grafana-User header with username into the request, default is false. +;send_user_header = false + +#################################### Analytics #################################### +[analytics] +# Server reporting, sends usage counters to stats.grafana.org every 24 hours. +# No ip addresses are being tracked, only simple counters to track +# running instances, dashboard and error counts. It is very helpful to us. +# Change this option to false to disable reporting. +;reporting_enabled = true + +# Set to false to disable all checks to https://grafana.net +# for new vesions (grafana itself and plugins), check is used +# in some UI views to notify that grafana or plugin update exists +# This option does not cause any auto updates, nor send any information +# only a GET request to http://grafana.com to get latest versions +;check_for_updates = true + +# Google Analytics universal tracking code, only enabled if you specify an id here +;google_analytics_ua_id = + +# Google Tag Manager ID, only enabled if you specify an id here +;google_tag_manager_id = + +#################################### Security #################################### +[security] +# disable creation of admin user on first start of grafana +;disable_initial_admin_creation = false + +# default admin user, created on startup +admin_user = {{ grafana_admin_user }} + +# default admin password, can be changed before first start of grafana, or in profile settings +admin_password = {{ grafana_admin_password }} + +# used for signing +;secret_key = SW2YcwTIb9zpOOhoPsMm + +# disable gravatar profile images +;disable_gravatar = false + +# data source proxy whitelist (ip_or_domain:port separated by spaces) +;data_source_proxy_whitelist = + +# disable protection against brute force login attempts +;disable_brute_force_login_protection = false + +# set to true if you host Grafana behind HTTPS. default is false. +;cookie_secure = false + +# set cookie SameSite attribute. defaults to `lax`. can be set to "lax", "strict" and "none" +;cookie_samesite = lax + +# set to true if you want to allow browsers to render Grafana in a ,