Import from elsewhere

This commit is contained in:
Théophile Bastian 2025-03-03 20:37:51 +01:00
parent 4017e9ac0c
commit 0f2bc19d5f
77 changed files with 1479 additions and 0 deletions

21
ansible.cfg Normal file
View file

@ -0,0 +1,21 @@
[defaults]
inventory = ./inventory/production
roles_path = ./roles
collections_path = ./.ansible/collections:/usr/share/ansible/collections
ansible_managed = "This file is managed by Ansible.
DO NOT MODIFY IT HERE, your changes will most likely be overwritten later.
You can find the Ansible repository here:
https://git.tobast.fr/tobast/dn42-ansible"
retry_files_enabled = false
interpreter_python = auto_silent
stdout_callback = yaml
[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False
[ssh_connection]
ssh_args = -C -o ControlMaster=auto -o ControlPersist=60s -o ControlPath=/tmp/ansible-socket-%u-to-%r@%h:%p

View file

@ -0,0 +1,7 @@
---
- hosts: "{{ current_host }}"
vars:
ansible_user: root
roles:
- ansible_base_deps
- sysadmin_users

View file

@ -0,0 +1,9 @@
---
- hosts: fully_managed
roles:
- etckeeper_commit
- common
- hosts: fully_managed:!no_nullmailer
roles:
- nullmailer

View file

@ -0,0 +1,5 @@
---
- hosts:
- fully_managed
roles:
- nftables

View file

@ -0,0 +1,4 @@
---
- hosts: nginx
roles:
- nginx

View file

@ -0,0 +1,7 @@
---
- hosts:
- dsi_users
roles:
- etckeeper_commit
- ansible_base_deps
- sysadmin_users

18
playbooks/remove_user.yml Normal file
View file

@ -0,0 +1,18 @@
# Removes the user passed as `username` from extra_vars, if it exists
- hosts: all
tasks:
- name: Etckeeper - commit
import_role:
name: etckeeper_commit
- name: "Remove user {{ username }}"
user:
name: "{{ username }}"
state: absent
remove: true
become: yes
- name: Etckeeper - commit
import_role:
name: etckeeper_commit
vars:
etckeeper_reason: "Remove user {{ username }}"

View file

@ -0,0 +1,5 @@
---
- hosts: debian
tasks:
- command: unattended-upgrade
become: yes

View file

@ -0,0 +1,20 @@
# Ansible base dependency role
This role is supposed to be deployed on every machine we have.
It ensures that all the distribution packages required by the Ansible modules
we are using are installed.
To ensures that other roles will not fail, it should be the first role to be
executed on a newly provisionned machine.
## Variables
* `required_packages`: this is a role variable. It cannot be overridden by
group- or host-variables. Defines the packages that should be installed.
## Note on required package names
These are **Debian** package names: if we happen, at some point, to have some
other distributions among our machines, we should change this role to reflect
this.

View file

@ -0,0 +1,12 @@
---
- name: install ansible package dependencies
apt:
name: "{{ required_packages }}"
update_cache: yes
become: yes
- name: Etckeeper - commit
import_role:
name: etckeeper_commit
vars:
etckeeper_reason: install base Ansible dependencies

View file

@ -0,0 +1,6 @@
---
required_packages:
- acl # Used for `copy` and `template` modules, see https://docs.ansible.com/ansible/latest/user_guide/become.html#becoming-an-unprivileged-user
- aptitude # Used for the `apt` module
- sudo # Used for the `become` module
- python3

10
roles/common/README.md Normal file
View file

@ -0,0 +1,10 @@
# Common
Ce rôle est le rôle de base, installé sur toutes les machines.
## Variables de configuration
* `common_locales`: locales à déployer
* `common_packages`: packages Debian à installer
* `common_firewall`: whether to deploy the `nftables` role as part of common.
Defaults to True.

View file

@ -0,0 +1,19 @@
---
common_locales:
- 'fr_FR.UTF-8 UTF-8'
- 'en_US.UTF-8 UTF-8'
common_packages:
- git
- htop
- lsb-release
- tcpdump
- nmap
- mtr-tiny
- dnsutils
- vim
- tmux
- less
- aptitude
common_firewall: True

View file

@ -0,0 +1,4 @@
---
- name: Generate locales
command: '/usr/sbin/locale-gen --keep-existing'
become: yes

View file

@ -0,0 +1,7 @@
---
dependencies:
- ansible_base_deps
- etckeeper
- sysadmin_users
- mollyguard
- fail2ban

View file

@ -0,0 +1,19 @@
---
- name: Configure locale.gen
template:
src: locale/locale.gen.j2
dest: /etc/locale.gen
become: yes
notify: Generate locales
- name: Configure locales
template:
src: locale/locale.conf.j2
dest: /etc/locale.conf
become: yes
- name: Etckeeper - commit
import_role:
name: etckeeper_commit
vars:
etckeeper_reason: Configure locales

View file

@ -0,0 +1,12 @@
---
- name: Install base packages
apt:
name: '{{ common_packages }}'
state: present
become: yes
- name: Etckeeper - commit
import_role:
name: etckeeper_commit
vars:
etckeeper_reason: install base packages

View file

@ -0,0 +1,16 @@
---
- name: Install base packages
import_tasks: install_base_packages.yml
- name: Configure locale
import_tasks: configure_locale.yml
- name: Install nftables
import_role:
name: nftables
when: common_firewall
- name: Install unattended_upgrades
import_role:
name: unattended_upgrades
when: "ansible_distribution == 'Debian'"

View file

@ -0,0 +1,2 @@
LANG=en_US.UTF-8
LC_TIME=fr_FR.UTF-8

View file

@ -0,0 +1,5 @@
{{ ansible_managed | comment }}
{% for locale in common_locales %}
{{ locale }}
{% endfor %}

View file

@ -0,0 +1,3 @@
# Etckeeper
Make git commit for changes to `/etc`.

View file

@ -0,0 +1,18 @@
---
etckeeper_scripts:
- name: 30initial-package-list
dir: /etc/etckeeper/init.d
- name: 10refresh-package-list
dir: /etc/etckeeper/post-install.d
- name: etckeeper_prompt.sh
dir: /etc/profile.d
etckeeper_ignore_paths:
- comment: ZFS zpool cache
path: zfs/zpool.cache
- comment: pve (Used by Proxmox)
path: pve/
- comment: lock for certbot
path: letsencrypt/.certbot.lock
etckeeper_group: 'adm'

View file

@ -0,0 +1,20 @@
---
- name: "etckeeper -- Ignore {{ item.comment }}"
lineinfile:
dest: /etc/.gitignore
regexp: "^{{ item.path }}$"
line: "{{ item.path }}"
become: yes
# git returns 0 if path was tracked, and 128 if path was already
# removed from the index. Don't fail in these two cases.
- name: "Remove {{ item.comment }} from git index (but don't actually remove it)"
command: git rm --cached -r {{ item.path }}
args:
chdir: /etc/
tags:
- skip_ansible_lint
register: git_result
failed_when: git_result.rc != 0 and git_result.rc != 128
changed_when: git_result.rc == 0
become: yes

View file

@ -0,0 +1,50 @@
---
# Installation
- name: Etckeeper - install packages
apt:
name:
- etckeeper
state: present
become: yes
- name: Etckeeper - ignore some paths
include_tasks: etckeeper_ignore_paths.yml
loop: "{{ etckeeper_ignore_paths }}"
loop_control:
label: "{{ item.comment }}"
- name: "etckeeper: install scripts"
template:
src: "scripts/{{ item.name }}"
dest: "{{ item.dir }}/{{ item.name }}"
owner: root
group: root
mode: 0755
loop: "{{ etckeeper_scripts }}"
become: yes
# Configuration
- name: Etckeeper - configure
template:
src: etckeeper/etckeeper.conf.j2
dest: /etc/etckeeper/etckeeper.conf
owner: root
group: root
mode: 0644
become: yes
- name: Etckeeper - configure sudoers
template:
src: sudoers/etckeeper.j2
dest: /etc/sudoers.d/etckeeper
owner: root
group: root
mode: 0644
become: yes
- name: Etckeeper - commit
import_role:
name: etckeeper_commit
vars:
etckeeper_reason: etckeeper configuration

View file

@ -0,0 +1,42 @@
{{ ansible_managed | comment }}
# The VCS to use.
VCS="git"
# Options passed to git commit when run by etckeeper.
GIT_COMMIT_OPTIONS=""
# Options passed to hg commit when run by etckeeper.
HG_COMMIT_OPTIONS=""
# Options passed to bzr commit when run by etckeeper.
BZR_COMMIT_OPTIONS=""
# Options passed to darcs record when run by etckeeper.
DARCS_COMMIT_OPTIONS="-a"
# Uncomment to avoid etckeeper committing existing changes
# to /etc automatically once per day.
AVOID_DAILY_AUTOCOMMITS=1
# Uncomment the following to avoid special file warning
# (the option is enabled automatically by cronjob regardless).
#AVOID_SPECIAL_FILE_WARNING=1
# Uncomment to avoid etckeeper committing existing changes to
# /etc before installation. It will cancel the installation,
# so you can commit the changes by hand.
AVOID_COMMIT_BEFORE_INSTALL=1
# The high-level package manager that's being used.
# (apt, pacman-g2, yum, zypper etc)
HIGHLEVEL_PACKAGE_MANAGER=apt
# The low-level package manager that's being used.
# (dpkg, rpm, pacman, pacman-g2, etc)
LOWLEVEL_PACKAGE_MANAGER=dpkg
# To push each commit to a remote, put the name of the remote here.
# (eg, "origin" for git). Space-separated lists of multiple remotes
# also work (eg, "origin gitlab github" for git).
PUSH_REMOTE=""

View file

@ -0,0 +1,7 @@
{{ ansible_managed | comment }}
#!/bin/sh
set -e
echo "# Generated by etckeeper. Do not edit." > .package-list
echo "" >> .package-list
etckeeper list-installed >> .package-list

View file

@ -0,0 +1,11 @@
{{ ansible_managed | comment }}
#!/bin/sh
set -e
if ! [ -e .package-list ]; then
echo "# Generated by etckeeper. Do not edit." > .package-list
echo "" >> .package-list
chmod 600 .package-list
etckeeper list-installed >> .package-list
fi

View file

@ -0,0 +1,98 @@
{{ ansible_managed | comment }}
# /etc/profile.d/etckeeper_prompt.sh
#
# Ce fragment de shell a vocation a être « sourcé » lors de l'initialisation de
# bash. Sous Debian, il suffit de l'ajouter dans /etc/profile.d
#
# Une fois en place quand une modification n'est pas publiée dans etckeeper, un
# '(!)' s'affiche dans le prompt.
IS_ETCKEEPER="no"
SUDO="sudo -n"
if [ "$(id -u)" = 0 ]; then
IS_ETCKEEPER="yes"
SUDO=""
elif id -nG | grep -qw "{{ etckeeper_group }}"; then
IS_ETCKEEPER="yes"
fi
if [ "$IS_ETCKEEPER" = "yes" ] ; then
case $- in
*i*) # interactive shell
etckeeper_unclean() {
if $SUDO etckeeper unclean 2> /dev/null; then
echo -n '(!) '
fi
}
etckeeper_prompt() {
if [ -z "$ETCKEEPER_PS1" ]; then
ETCKEEPER_PS1="$PS1"
fi
PS1="\$(etckeeper_unclean)$ETCKEEPER_PS1"
}
etckeeper_prompt_color() {
if [ -z "$ETCKEEPER_PS1" ]; then
ETCKEEPER_PS1="$PS1"
fi
PS1="\[\e[1;31m\]\$(etckeeper_unclean)\[\033[00m\]$ETCKEEPER_PS1"
}
git_export_env() {
local ttyuser ttyuserhome conf
ttyuser="$(stat -c "%U" $(tty))"
ttyuserhome="$(getent passwd "$ttyuser" | cut -d: -f6)"
conf="$ttyuserhome/.gitconfig"
if [ -z "$GIT_AUTHOR_NAME" ] && [ -z "$GIT_AUTHOR_EMAIL" ]; then
if [ ! -z "$GIT_CONFIG_LOCAL" ] || [ ! -z "$GIT_CONFIG" ]; then
export GIT_AUTHOR_NAME="$(git config --get user.name)"
export GIT_AUTHOR_EMAIL="$(git config --get user.email)"
elif [ -r "$conf" ]; then
export GIT_AUTHOR_NAME="$(git config --file "$conf" --get user.name)"
export GIT_AUTHOR_EMAIL="$(git config --file "$conf" --get user.email)"
else
export GIT_AUTHOR_NAME="$USER"
export GIT_AUTHOR_EMAIL="$USER@$(hostname -f)"
fi
fi
}
check_uncommitted() {
if $SUDO etckeeper unclean 2> /dev/null; then
echo "Uncommitted changes to /etc found, please commit them"
CHECK_UNCOMMITED="done" bash -l$-
fi
}
if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then
# We have color support; assume it's compliant with Ecma-48
# (ISO/IEC-6429). (Lack of such support is extremely rare, and such
# a case would tend to support setf rather than setaf.)
color_prompt=yes
else
color_prompt=
fi
if [ "$color_prompt" = "yes" ]; then
PROMPT_COMMAND=etckeeper_prompt_color
else
PROMPT_COMMAND=etckeeper_prompt
fi
if [ "$CHECK_UNCOMMITED" != "done" ]; then
trap check_uncommitted EXIT
fi
git_export_env
;;
esac
fi
unset IS_ETCKEEPER

View file

@ -0,0 +1,4 @@
{{ ansible_managed | comment }}
%{{ etckeeper_group }} ALL=(ALL) NOPASSWD: /usr/sbin/etckeeper unclean
%{{ etckeeper_group }} ALL=(ALL) NOPASSWD: /usr/bin/etckeeper unclean

View file

@ -0,0 +1,2 @@
---
etckeeper_reason: 'autocommit'

View file

@ -0,0 +1,12 @@
---
- name: Etckeeper - check if commit is needed
command: etckeeper unclean
register: etckeeper_result
failed_when: etckeeper_result.rc not in [0, 1, 2]
changed_when: etckeeper_result.rc == 0
become: yes
- name: Etckeeper - commit
command: '/usr/bin/etckeeper commit -m "Ansible: {{ etckeeper_reason }}"'
when: etckeeper_result.changed
become: yes

9
roles/fail2ban/README.md Normal file
View file

@ -0,0 +1,9 @@
# fail2ban
Fail2ban monitors the system to ban, through the firewall, hosts that are
offending certain rules (eg. trying to `ssh` too many times in a row with
incorrect credentials).
## Variables
* `fail2ban_ssh_dsi_ip`: IPs that must not be blocked.

View file

@ -0,0 +1,2 @@
---
fail2ban_ssh_dsi_ip: 10.35.36.0/24

View file

@ -0,0 +1,6 @@
---
- name: reload fail2ban
service:
name: fail2ban
state: reloaded
become: yes

View file

@ -0,0 +1,24 @@
---
- name: install fail2ban
block:
- apt:
name: fail2ban
- service:
name: fail2ban
state: started
enabled: yes
become: yes
- name: configure fail2ban
template:
src: jail.d/defaults-echirolles.conf.j2
dest: /etc/fail2ban/jail.d/defaults-echirolles.conf
become: yes
notify:
- reload fail2ban
- name: Etckeeper - commit
import_role:
name: etckeeper_commit
vars:
etckeeper_reason: Install and configure Fail2ban

View file

@ -0,0 +1,10 @@
{{ ansible_managed | comment }}
[DEFAULT]
banaction = nftables
banaction_allports = nftables[type=allports]
[sshd]
enabled = true
backend = systemd
ignoreip = {{ fail2ban_ssh_dsi_ip }}

36
roles/nftables/README.md Normal file
View file

@ -0,0 +1,36 @@
# iptables
Defines an `nftables` firewall.
## Allowing services
To allow a certain service that should not be allowed on all of our machines,
we use a `firewall_allow_[service]` construct.
In the filters template file, we guard the specific rule with a
`{% if firewall_allow_[service] %}`. This variable should be set to `false` in
the `default_vars` of the present role.
To allow this service, this variable must be overriden to `true` in the
`group_vars` of the relevant group.
The files under `/etc/nftables/local/` are included in the relevant chins.
## Firewalls **MUST** be tested!
When this role is deployed to a host, the firewall configuration is
**immediately** applied, and will be applied upon reboot. If you mess up with
this, we might end up locked out of our machine.
**Please**, test this role **extensively** whenever you modify it. Someone else
might end up deploying it if you do not specify clearly and explicitly that
your modifications are not ready for production.
## Variables
### `firewall_allow_*` vars
All these defaults to false and should be overriden as stated above.
* `firewall_allow_http`: allow tcp ports 80/443 from everywhere
* `firewall_allow_dns`: allow tcp and udp port 53

View file

@ -0,0 +1,5 @@
---
# Service accesses are disallowed by default. They are set to true in
# group_vars for groups that need them to be available.
firewall_allow_http: false
firewall_allow_dns: false

View file

@ -0,0 +1,6 @@
---
- name: reload nftables
service:
name: nftables
state: reloaded
become: yes

View file

@ -0,0 +1,41 @@
---
- name: install nftables firewall
apt:
name: nftables
- name: make nftables configuration directories
file:
path: "/etc/nftables/{{ item }}"
state: directory
owner: root
group: adm
mode: 0750
items:
- ''
- 'toplevel.d'
- 'filter-toplevel.d'
- 'filter-input.d'
- 'filter-forward.d'
- 'filter-output.d'
- name: deploy nftables root configuration
template:
src: "nftables/nftables.conf.j2"
dest: "/etc/nftables.conf"
owner: root
group: adm
mode: 0640
notify: reload nftables
- name: enable nftables service
systemd:
# If we do not call daemon-reload, systemd can somehow fail to find the
# nftables service.
name: nftables
state: started
enabled: true
daemon-reload: true
- name: Etckeeper - commit
import_role:
name: etckeeper_commit
vars:
etckeeper_reason: install and configure nftables firewall

View file

@ -0,0 +1,50 @@
#!/usr/sbin/nft -f
{{ ansible_managed | comment }}
flush ruleset
include "/etc/nftables/toplevel.d/*.conf"
table inet filter {
include "/etc/nftables/filter-toplevel.d/*.conf"
chain input {
type filter hook input priority 0; policy drop;
iifname "lo" accept comment "Accept localhost traffict"
iifname != "lo" ip daddr 127.0.0.0/8 reject comment "Spoofed lo"
ct state related,established accept
meta l4proto icmp accept
meta l4proto igmp accept
meta l4proto ipv6-icmp accept
include "/etc/nftables/filter-input.d/*.conf"
meta l4proto tcp tcp dport 22 accept
{% if firewall_allow_http %}
# Web server (TCP/80,443)
tcp dport 80 accept
tcp dport 443 accept
{% endif %}
{% if firewall_allow_dns %}
# DNS
tcp dport 53 accept
udp dport 53 accept
{% endif %}
}
chain forward {
type filter hook forward priority 0; policy drop;
include "/etc/nftables/filter-forward.d/*.conf"
}
chain output {
type filter hook output priority 0; policy accept;
include "/etc/nftables/filter-output.d/*.conf"
}
}

16
roles/nginx/README.md Normal file
View file

@ -0,0 +1,16 @@
# Nginx
Nginx is a web server and reverse-proxy.
This role configures the base server, other roles should be used to configure
its various servers (ie. configuration files to serve content in a certain way
over a certain domain name).
## Important note
If you wish a certain host to embed this role, you should add it to the `nginx`
group. Otherwise, you might end up with the ports firewalled out.
## Variables
None.

View file

@ -0,0 +1,6 @@
---
- name: reload nginx
service:
name: nginx
state: reloaded
become: yes

View file

@ -0,0 +1,3 @@
---
dependencies:
- role: fail2ban

View file

@ -0,0 +1,73 @@
---
- name: install nginx
apt:
name: nginx
- name: add gitignore
template:
src: nginx/gitignore.j2
dest: /etc/nginx/.gitignore
- name: create snippets directory
file:
path: /etc/nginx/include
state: directory
- name: add configuration snippets
template:
src: "nginx/include/{{ item }}.j2"
dest: "/etc/nginx/include/{{ item }}"
loop:
- php
- php_with_upstream
- proxy_params
- redirect_ssl
- ssl_conf
- static
- name: "add ssl_keys_* files"
template:
src: "nginx/include/ssl_keys.j2"
dest: "/etc/nginx/include/ssl_keys_{{ item }}"
loop: "{{ ssl_domains | mandatory | map('first') | flatten }}"
when: ssl_domains is defined
vars:
- cert_name: "{{ item }}"
- name: create keys directory
file:
path: /etc/nginx/keys
state: directory
owner: root
group: root
mode: 0750
- name: generate dhparam
openssl_dhparam:
path: /etc/nginx/keys/dhparam.pem
force: no
owner: root
group: root
mode: 0640
size: 2048
- name: add default server
template:
src: nginx/sites-available/default_server.j2
dest: /etc/nginx/sites-available/default_server
notify: reload nginx
- name: enable default server
file:
path: /etc/nginx/sites-enabled/default_server
state: link
src: '../sites-available/default_server'
notify: reload nginx
- name: disable nginx stock default server
file:
path: /etc/nginx/sites-enabled/default
state: absent
notify: reload nginx
- name: configure fail2ban
template:
src: fail2ban/nginx.conf.j2
dest: /etc/fail2ban/jail.d/nginx.conf
notify: reload fail2ban
- name: Etckeeper - commit
import_role:
name: etckeeper_commit
vars:
etckeeper_reason: install and base configuration of nginx

View file

@ -0,0 +1,7 @@
{{ ansible_managed | comment }}
[nginx-limit-req]
enabled = true
[nginx-botsearch]
enabled = true

View file

@ -0,0 +1,3 @@
{{ ansible_managed | comment }}
# This file is provided in case the system has an etckeeper
keys

View file

@ -0,0 +1,6 @@
{{ ansible_managed | comment }}
location /.well-known/acme-challenge {
alias /var/lib/dehydrated/acme-challenges/;
default_type 'text/plain';
}

View file

@ -0,0 +1,4 @@
{{ ansible_managed | comment }}
# Implements requests rate limit -- see conf.d/limit_req_zone.conf
limit_req zone=all_sites burst=5;

View file

@ -0,0 +1,10 @@
{{ ansible_managed | comment }}
# HOW TO (typically) USE THIS FILE:
#
# location ~ [^/]\.php(/|$) {
# include include/php;
# }
set $php_upstream php;
include include/php_with_upstream;

View file

@ -0,0 +1,14 @@
{{ ansible_managed | comment }}
# HOW TO (typically) USE THIS FILE:
#
# location ~ [^/]\.php(/|$) {
# set $php_upstream my_blah_upstream;
# include include/php_with_upstream;
# }
include fastcgi.conf;
fastcgi_intercept_errors on;
fastcgi_pass $php_upstream;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;

View file

@ -0,0 +1,10 @@
{{ ansible_managed | comment }}
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-SSL-Client-Serial $ssl_client_serial;
proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
proxy_set_header X-SSL-Client-S-DN $ssl_client_s_dn;
proxy_pass_header Authorization;

View file

@ -0,0 +1,6 @@
{{ ansible_managed | comment }}
include include/letsencrypt-challenge;
location / {
return 301 https://$server_name$request_uri;
}

View file

@ -0,0 +1,24 @@
{{ ansible_managed | comment }}
include include/letsencrypt-challenge;
# Taken from https://mozilla.github.io/server-side-tls/ssl-config-generator/
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m; # about 40000 sessions
ssl_session_tickets off;
# Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits
ssl_dhparam /etc/nginx/keys/dhparam.pem;
# intermediate configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# HSTS (ngx_http_headers_module is required) (63072000 seconds)
add_header Strict-Transport-Security "max-age=63072000" always;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;

View file

@ -0,0 +1,10 @@
{{ ansible_managed | comment }}
# Defines the SSL certificates and every SSL security parameter (through the
# inclusion of `include/ssl_conf`)
include include/ssl_conf;
## Dehydrated
ssl_certificate /etc/dehydrated/certs/{{ cert_name }}/fullchain.pem;
ssl_certificate_key /etc/dehydrated/certs/{{ cert_name }}/privkey.pem;

View file

@ -0,0 +1,5 @@
{{ ansible_managed | comment }}
access_log off;
add_header Cache-Control "public";
expires 7d;

View file

@ -0,0 +1,15 @@
{{ ansible_managed | comment }}
server {
server_name _;
listen 80 default_server;
listen [::]:80 default_server;
include include/letsencrypt-challenge;
access_log /var/log/nginx/access.default_server.log;
location / {
return 404;
}
}

View file

@ -0,0 +1,36 @@
# Sysadmin users
Creates UNIX users and home directories for each sysadmin. Also takes care of
disabling former sysadmin users.
## Variables
* `sysadmins`: list of usernames of the current sysadmins, ie. people that have
an account on the various machines and are sudoers. This value can be
overridden to have different sysadmins on a specific machine.
Please refer to `defaults/main.yml` for the current default value.
* `former_sysadmins`: list of usernames of the former sysadmins, ie. people
that used to have a sudoer account on the machines and must now be disabled
(ie., cannot log in anymore).
Please refer to `defaults/main.yml` for the current default value.
* `sysadmins_groups`: additional groups in which the sysadmins belong. Defaults
to `adm sudonopass`.
### Role variables (cannot be overridden)
* `sysadmins_details`: dictionary mapping sysadmin or former sysadmin logins
to the given user's configuration.
This variable is set as a role variable: we might want different sysadmins on
various machines, but we want a consistent configuration of the defined
users.
Each entry contains:
* `full_name`: full name of the sysadmin.
* `email`: email address of the sysadmin
* `ssh_keys`: list of ssh public keys. **PLEASE KEEP IN MIND** that it is
your responsability to keep your private key safe. In particular, once you
add a computer's public key here, always lock your computer and have a
strong passphrase.
* `shell`: shell this user wants to use.
* `deploy_files`: list of `src`/`dest` entries that will be deployed from
this role's `files` to the user's home directory.

View file

@ -0,0 +1,8 @@
---
sysadmins:
- tobast
former_sysadmins: []
sysadmins_sudo_group: 'sudonopass'
sysadmins_groups:
- adm
- '{{ sysadmins_sudo_group }}'

View file

@ -0,0 +1,63 @@
# Set $TERM env
set -g default-terminal "screen-256color"
# No escape delay, so that one can still use vim
set -sg escape-time 0
# Set prefix to ^A
unbind C-b
set -g prefix C-a
bind C-a send-prefix
# Navigation
## Navigation in panes with C+hjkl integrated with vim
bind -n C-h run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys C-h) || tmux select-pane -L"
bind -n C-j run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys C-j) || tmux select-pane -D"
bind -n C-k run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys C-k) || tmux select-pane -U"
bind -n C-l run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys C-l) || tmux select-pane -R"
## Navigation in windows with alt+shift+jk
bind -n M-j select-window -p
bind -n M-k select-window -n
# Resizing
## Resize panes through C-M-{hjkl}
bind -n C-M-h resize-pane -L
bind -n C-M-j resize-pane -D
bind -n C-M-k resize-pane -U
bind -n C-M-l resize-pane -R
# Splitting
unbind %
unbind '"'
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"
# New windows use the same working directory
bind c new-window -c "#{pane_current_path}"
# Set window notifications
setw -g monitor-activity on
set -g visual-activity on
# Reload tmux conf
unbind r
bind r source-file ~/.tmux.conf \; display-message " Config reloaded".
# Reassign overloaded bindings
bind l send-keys 'C-l'
# Status bar
set -g status-style bg=colour237
set -ag status-style fg=colour216
set -ag status-style dim
set-option -g status-left '#[fg=colour196][#[fg=default]#S#[fg=colour196]]#[fg=default] '
set-option -g status-right \
'#[fg=colour196][#[fg=default]#(echo $USER)#[fg=colour196]@\
#[fg=default]#h#[fg=colour196]]#[fg=default] \
#[fg=colour196][#[fg=default]#(date "+%H:%M %F")#[fg=colour196]]#[fg=defaultb'
# Current window
set-window-option -g window-status-current-style bg=colour215
set-window-option -ag window-status-current-style fg=colour88
set-window-option -g window-status-current-format ' #I:#W#F '

View file

@ -0,0 +1,77 @@
" Tiny vimrc without plugins, to be deployed on servers
filetype plugin on
syntax on
set colorcolumn=80
set background=dark
set number
set relativenumber
set tabstop=4
"set softtabstop=4
set shiftwidth=4
set expandtab
set smartindent
set incsearch
set ttimeoutlen=100
set scrolloff=5
set tw=79
" Leader
let mapleader = "\<Space>"
let maplocalleader = "\<Space>"
" Tab completion
set wildmode=longest,list,full
set wildmenu
" Splitting
set splitbelow
set splitright
" Split delimiter
set fillchars+=vert:\│
hi! VertSplit ctermbg=243 ctermfg=16
hi! SignColumn ctermbg=16
set exrc
set secure
if has("autocmd")
au BufReadPost * if line("'\"") > 0 && line("'\"") <= line("$")
\| exe "normal! g'\"" | endif
endif
filetype plugin indent on " required
""""""""""""""""" Trailing whitespaces
fun! TrimWhitespace()
let l:save_cursor = getpos('.')
%s/\s\+$//e
call setpos('.', l:save_cursor)
endfun
" Show trailing whitepace and spaces before a tab:
highlight ExtraWhitespace ctermbg=darkgreen guibg=lightgreen
match ExtraWhitespace /\s\+$\| \+\ze\t/
autocmd Syntax * syn match ExtraWhitespace /\s\+$\| \+\ze\t/
""""""""""""""""" Key bindings
nnoremap <F1> :w<CR>:!make<CR>
inoremap <F1> <esc>:echo 'F1 help disabled'<CR>
nnoremap <F2> :!git status<CR>
nnoremap __ :lprev<CR>
nnoremap ++ :lnext<CR>
nnoremap _+ :ll<CR>
nnoremap <Leader>w :call TrimWhitespace()<CR>
xnoremap <Leader>x <esc>:'<,'>:w !xclip -selection clipboard<CR><esc>
nnoremap <Leader>p <esc>:set invpaste<CR>
cmap w!! w !sudo tee % > /dev/null
" Disable arrow keys - use hjkl, ffs.
noremap <Up> <NOP>
noremap <Down> <NOP>
noremap <Left> <NOP>
noremap <Right> <NOP>

View file

@ -0,0 +1,37 @@
# Create a sysadmin user. Arguments:
# * `username`: username of the sysadmin
# * `details`: details of the sysadmin
---
- name: "create sysadmin user {{ username }}"
user:
name: "{{ username }}"
create_home: yes
append: true
groups: "{{ sysadmins_groups }}"
home: "/home/{{ username }}"
shell: "{{ details.shell }}"
expires: -1 # Remove expiracy, in case we re-enabled a former sysadmin
password_lock: no # idem
state: present
become: yes
- name: "set {{ username }}'s public SSH keys"
authorized_key:
key: "{{ details.ssh_keys|join('\n') }}"
user: "{{ username }}"
exclusive: yes
become: yes
become_user: "{{ username }}"
tags:
- update_files
- name: "deploy {{ username }}'s files"
copy:
src: "{{ cur_file.src }}"
dest: "/home/{{ username }}/{{ cur_file.dest }}"
loop: "{{ details.deploy_files }}"
loop_control:
loop_var: cur_file
become: yes
become_user: "{{ username }}"
tags:
- update_files

View file

@ -0,0 +1,19 @@
# Disable an existing sysadmin user, which is now a former sysadmin. Arguments:
# * `username`: username of the sysadmin
# * `details`: details of the sysadmin
---
- name: "unset {{ username }}'s public SSH keys"
authorized_key:
key: ""
user: "{{ username }}"
exclusive: yes
become: yes
become_user: "{{ username }}"
- name: "expire user account {{ username }}"
user:
name: "{{ item }}"
password_lock: yes
groups: ""
expires: 1
become: yes

View file

@ -0,0 +1,38 @@
---
- name: create sysadmin sudoer group
group:
name: '{{ sysadmins_sudo_group }}'
state: present
become: yes
- name: add sysadmin group to sudoers
template:
src: sudoers.d/sysadmins.j2
dest: /etc/sudoers.d/sysadmins
become: yes
- name: create sysadmin users
include_role:
name: sysadmin_users
tasks_from: create_sysadmin
vars:
- username: "{{ item }}"
- details: "{{ sysadmin_details[item] }}"
loop: "{{ sysadmins }}"
tags:
- update_files
- name: disable former sysadmins
include_role:
name: sysadmin_users
tasks_from: disable_sysadmin
vars:
- username: "{{ item }}"
- details: "{{ sysadmin_details[item] }}"
loop: "{{ former_sysadmins }}"
- name: Etckeeper - commit
import_role:
name: etckeeper_commit
vars:
etckeeper_reason: update sysadmin users

View file

@ -0,0 +1,14 @@
{{ ansible_managed | comment }}
{% if sysadmins_root_ssh_keys_others %}
# Additional specific keys for this machine
{% for key in sysadmins_root_ssh_keys_others %}{{ key }}
{% endfor %}
{% endif %}
{% for sysadmin in sysadmins %}
# {{ sysadmin_details[sysadmin].full_name }} "{{ sysadmin }}" <{{ sysadmin_details[sysadmin].email }}>
{% for key in sysadmin_details[sysadmin].ssh_keys %}{{ key }}
{% endfor %}
{% endfor %}

View file

@ -0,0 +1,3 @@
{{ ansible_managed | comment }}
%{{ sysadmins_sudo_group }} ALL=(ALL:ALL) NOPASSWD: ALL

View file

@ -0,0 +1,14 @@
---
sysadmin_details:
tobast:
full_name: "tobast"
email: "contact+dn42@tobast.fr"
ssh_keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMcfT+BBD1b9ZbJHPwfzdFVNlliZZEg0JaID+u0z2VJS tobast@idefix"
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMI7S6w08j5AVfw7hGHGyjnycoCPukCJaiAByVVGVQym tobast@popcorn"
shell: "/bin/bash"
deploy_files:
- src: tobast/tmux.conf
dest: '.tmux.conf'
- src: tobast/vimrc
dest: '.vimrc'

View file

@ -0,0 +1,7 @@
# Unattended-upgrades
This service ensures that the security updates are installed daily.
## Variables
None.

View file

@ -0,0 +1,29 @@
---
- name: check whether Debian is running
assert:
that:
- "ansible_distribution == 'Debian'"
fail_msg: "Unattended-upgrades is only intended for Debian.
We expected only Debian servers managed by this Ansible"
- name: install unattended-upgrades
apt:
name:
- unattended-upgrades
- apt-listchanges
become: yes
- name: configure unattended-upgrades
template:
src: 50unattended-upgrades.j2
dest: /etc/apt/apt.conf.d/50unattended-upgrades
become: yes
- name: enable unattended-upgrades
template:
src: 20auto-upgrades.j2
dest: /etc/apt/apt.conf.d/20auto-upgrades
become: yes
- name: Etckeeper - commit
import_role:
name: etckeeper_commit
vars:
etckeeper_reason: install and configure unattended-upgrades

View file

@ -0,0 +1,4 @@
{{ ansible_managed | comment('c') }}
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";

View file

@ -0,0 +1,105 @@
{{ ansible_managed | comment('c') }}
// Unattended-Upgrade::Origins-Pattern controls which packages are
// upgraded.
//
// Lines below have the format format is "keyword=value,...". A
// package will be upgraded only if the values in its metadata match
// all the supplied keywords in a line. (In other words, omitted
// keywords are wild cards.) The keywords originate from the Release
// file, but several aliases are accepted. The accepted keywords are:
// a,archive,suite (eg, "stable")
// c,component (eg, "main", "contrib", "non-free")
// l,label (eg, "Debian", "Debian-Security")
// o,origin (eg, "Debian", "Unofficial Multimedia Packages")
// n,codename (eg, "jessie", "jessie-updates")
// site (eg, "http.debian.net")
// The available values on the system are printed by the command
// "apt-cache policy", and can be debugged by running
// "unattended-upgrades -d" and looking at the log file.
//
// Within lines unattended-upgrades allows 2 macros whose values are
// derived from /etc/debian_version:
// ${distro_id} Installed origin.
// ${distro_codename} Installed codename (eg, "jessie")
Unattended-Upgrade::Origins-Pattern {
// Codename based matching:
// This will follow the migration of a release through different
// archives (e.g. from testing to stable and later oldstable).
// "o=Debian,n=jessie";
// "o=Debian,n=jessie-updates";
// "o=Debian,n=jessie-proposed-updates";
// "o=Debian,n=jessie,l=Debian-Security";
// Archive or Suite based matching:
// Note that this will silently match a different release after
// migration to the specified archive (e.g. testing becomes the
// new stable).
// "o=Debian,a=stable";
// "o=Debian,a=stable-updates";
// "o=Debian,a=proposed-updates";
"origin=Debian,codename=${distro_codename},label=Debian-Security";
"origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
};
// List of packages to not update (regexp are supported)
Unattended-Upgrade::Package-Blacklist {
// "vim";
// "libc6";
// "libc6-dev";
// "libc6-i686";
};
// This option allows you to control if on a unclean dpkg exit
// unattended-upgrades will automatically run
// dpkg --force-confold --configure -a
// The default is true, to ensure updates keep getting installed
//Unattended-Upgrade::AutoFixInterruptedDpkg "false";
// Split the upgrade into the smallest possible chunks so that
// they can be interrupted with SIGUSR1. This makes the upgrade
// a bit slower but it has the benefit that shutdown while a upgrade
// is running is possible (with a small delay)
//Unattended-Upgrade::MinimalSteps "true";
// Install all unattended-upgrades when the machine is shuting down
// instead of doing it in the background while the machine is running
// This will (obviously) make shutdown slower
//Unattended-Upgrade::InstallOnShutdown "true";
// Send email to this address for problems or packages upgrades
// If empty or unset then no email is sent, make sure that you
// have a working mail setup on your system. A package that provides
// 'mailx' must be installed. E.g. "user@example.com"
Unattended-Upgrade::Mail "root";
// Set this value to "true" to get emails only on errors. Default
// is to always send a mail if Unattended-Upgrade::Mail is set
Unattended-Upgrade::MailOnlyOnError "true";
// Do automatic removal of new unused dependencies after the upgrade
// (equivalent to apt-get autoremove)
//Unattended-Upgrade::Remove-Unused-Dependencies "false";
// Automatically reboot *WITHOUT CONFIRMATION* if
// the file /var/run/reboot-required is found after the upgrade
//Unattended-Upgrade::Automatic-Reboot "false";
// Automatically reboot even if there are users currently logged in.
//Unattended-Upgrade::Automatic-Reboot-WithUsers "true";
// If automatic reboot is enabled and needed, reboot at the specific
// time instead of immediately
// Default: "now"
//Unattended-Upgrade::Automatic-Reboot-Time "02:00";
// Use apt bandwidth limit feature, this example limits the download
// speed to 70kb/sec
//Acquire::http::Dl-Limit "70";
// Enable logging to syslog. Default is False
// Unattended-Upgrade::SyslogEnable "false";
// Specify syslog facility. Default is daemon
// Unattended-Upgrade::SyslogFacility "daemon";

22
roles/wireguard/README.md Normal file
View file

@ -0,0 +1,22 @@
# Wireguard
Définit un réseau Wireguard avec les pairs définis.
## Variables
* `wg_if_name`: nom de l'interface Wireguard à créer.
* `wg_port`: port à utiliser pour la communication Wireguard. Par défaut,
`51810`.
* `wg_addr`: adresse IP à utiliser sur cette interface, avec son préfixe CIDR.
* `wg_keepalive`: `true` pour envoyer des keepalive réguliers. Par défaut,
`false`.
* `wg_peers`: liste des pairs wireguard à configurer (également configurés par
ce rôle). Chaque entrée de cette liste est le nom du pair dans l'inventaire ;
* `wg_extra_hosts`: liste des pairs wireguard à configurer qui ne sont pas
gérés par ce rôle ansible. Chaque entrée de cette liste est un dictionnaire
contenant :
* `host`: nom de la machine ;
* `hostaddr`: adresse de la machine dans le réseau wireguard, sans préfixe
* `pk`: clé publique de la machine ;
* `endpoint`: optionnel. Adresse à laquelle cette machine peut être jointe
pour initier la connexion wireguard.

View file

@ -0,0 +1,4 @@
---
wg_port: 51810
wg_keepalive: false
wg_extra_hosts: []

View file

@ -0,0 +1,10 @@
---
- name: restart wireguard
systemd:
name: "wg-quick@{{ wg_if_name }}"
state: restarted
become: yes
- name: wireguard daemon-reload
systemd:
daemon-reload: yes
become: yes

View file

@ -0,0 +1,77 @@
---
### Install Wireguard
- name: "Install wireguard"
apt:
name:
- wireguard
- wireguard-tools
become: yes
### Generate keys
- name: Create keys directory
file:
path: '/etc/wireguard/keys'
state: directory
owner: root
mode: 0700
become: yes
- name: Generate keypair
shell:
chdir: '/etc/wireguard/keys'
cmd: "wg genkey | tee /etc/wireguard/keys/{{ wg_if_name }}-private.key
| wg pubkey > /etc/wireguard/keys/{{ wg_if_name }}-public.key"
creates: "/etc/wireguard/keys/{{ wg_if_name }}-private.key"
# ^^^ n'est pas exécuté si ce fichier existe déjà.
become: yes
- name: Set private key file permissions
file:
path: "/etc/wireguard/keys/{{ wg_if_name }}-private.key"
owner: root
mode: 0600
state: file
become: yes
- name: Read public key file
slurp:
src: /etc/wireguard/keys/{{ wg_if_name }}-public.key
become: yes
register: wireguard_public_key_file
- name: Set public key as a host fact
set_fact:
wg_public_key: "{{ wireguard_public_key_file['content']
| b64decode | regex_replace('\n$', '') }}"
### Deploy Wireguard configuration
- name: Create wg-quick service override directory
file:
path: "/etc/systemd/system/wg-quick@.service.d/"
state: directory
owner: root
mode: 0755
become: yes
- name: Install wg-quick service override
template:
src: systemd/wg-quick-override.conf.j2
dest: "/etc/systemd/system/wg-quick@.service.d/load-privkey.conf"
become: yes
notify: wireguard daemon-reload
- name: Install wireguard configuration
template:
src: wireguard/wg.conf.j2
dest: "/etc/wireguard/{{ wg_if_name }}.conf"
become: yes
register: wg_wireguard_config_file
notify: restart wireguard
- name: Enable wireguard
systemd:
name: "wg-quick@{{ wg_if_name }}"
enabled: yes
become: yes
- name: Etckeeper - commit
import_role:
name: etckeeper_commit
vars:
etckeeper_reason: "Configure wireguard for {{ wg_if_name }}"

View file

@ -0,0 +1,5 @@
{{ ansible_managed | comment }}
[Service]
# Load private key from file, if it exists
ExecStartPost=-/usr/bin/wg set %i private-key /etc/wireguard/keys/{{ wg_if_name }}-private.key

View file

@ -0,0 +1,31 @@
{{ ansible_managed | comment }}
[Interface]
ListenPort = {{ wg_port }}
Address = {{ wg_addr }}
{% for peer in wg_peers %}
# {{ peer }}
[Peer]
PublicKey = {{ hostvars[peer].wg_public_key }}
Endpoint = {{ hostvars[peer].inventory_hostname}}:{{ wg_port }}
AllowedIPs = {{ hostvars[peer].wg_addr | ipaddr('address') }}/128
{% if wg_keepalive -%}
PersistentKeepalive = 25
{%- endif %}
{% endfor %}
{% for peer in wg_extra_hosts %}
# {{ peer['host'] }}
[Peer]
PublicKey = {{ peer['pk'] }}
AllowedIPs = {{ peer['hostaddr'] }}/128
{% if 'endpoint' in peer %}
Endpoint = {{ peer['endpoint'] }}:{{ wg_port }}
{% endif %}
{% if wg_keepalive -%}
PersistentKeepalive = 25
{%- endif %}
{% endfor %}