init
This commit is contained in:
		
						commit
						4875e6ef1f
					
				|  | @ -0,0 +1,21 @@ | ||||||
|  | MIT License | ||||||
|  | 
 | ||||||
|  | Copyright (c) 2021 AnsibleGuy | ||||||
|  | 
 | ||||||
|  | Permission is hereby granted, free of charge, to any person obtaining a copy | ||||||
|  | of this software and associated documentation files (the "Software"), to deal | ||||||
|  | in the Software without restriction, including without limitation the rights | ||||||
|  | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||||
|  | copies of the Software, and to permit persons to whom the Software is | ||||||
|  | furnished to do so, subject to the following conditions: | ||||||
|  | 
 | ||||||
|  | The above copyright notice and this permission notice shall be included in all | ||||||
|  | copies or substantial portions of the Software. | ||||||
|  | 
 | ||||||
|  | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||||
|  | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||||
|  | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||||
|  | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||||
|  | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||||
|  | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||||
|  | SOFTWARE. | ||||||
|  | @ -0,0 +1,50 @@ | ||||||
|  | # Apache2 Ansible Role | ||||||
|  | Ansible role to install apache2 sites on the target server. | ||||||
|  | 
 | ||||||
|  | **Tested:** | ||||||
|  | * Debian 11 | ||||||
|  | 
 | ||||||
|  | ## Functionality | ||||||
|  | 
 | ||||||
|  | * Package installation | ||||||
|  |   * Ansible dependencies (_minimal_) | ||||||
|  |   * Apache2 | ||||||
|  | * Configuration | ||||||
|  |   *  | ||||||
|  |   * Default opt-in: | ||||||
|  |     *  | ||||||
|  |   * Default opt-outs: | ||||||
|  |     *  | ||||||
|  |   * Default config: | ||||||
|  |     *  | ||||||
|  | 
 | ||||||
|  | ## Info | ||||||
|  | 
 | ||||||
|  | * **Note:** Most of this functionality can be opted in or out using the main defaults file and variables! | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | * **Note:** this role currently only supports debian-based systems | ||||||
|  | 
 | ||||||
|  | ## Requirements | ||||||
|  | 
 | ||||||
|  | * Community collection: ```ansible-galaxy install -r requirements.yml``` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## Usage | ||||||
|  | Run the playbook: | ||||||
|  | ```bash | ||||||
|  | ansible-playbook -K -D -i inventory/hosts.yml playbook.yml | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | You need to define your instances by configuring the 'mariadb' dictionary! | ||||||
|  | 
 | ||||||
|  | ```yaml | ||||||
|  | apache | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | There are also some useful **tags** available: | ||||||
|  | * base => only configure basics; sites will not be touched | ||||||
|  | * sites | ||||||
|  | * config => configuration (base and instances) | ||||||
|  | * certs | ||||||
|  | @ -0,0 +1,148 @@ | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | # main switches | ||||||
|  | configure_anti_ddos: true  # mod_evasive | ||||||
|  | configure_security: true  # https://www.digitalocean.com/community/tutorials/how-to-set-up-mod_security-with-apache-on-debian-ubuntu | ||||||
|  | 
 | ||||||
|  | # default config => is overwritten by provided config | ||||||
|  | default_apache: | ||||||
|  |   sites: {} | ||||||
|  | 
 | ||||||
|  |   log: | ||||||
|  |     path: '/var/log/apache2' | ||||||
|  |     per_site: true | ||||||
|  |     syslog: true | ||||||
|  |     syslog_host: | ||||||
|  |     syslog_port: 514 | ||||||
|  |     syslog_max_size: '4KiB'  # see: https://manpages.ubuntu.com/manpages/xenial/man1/logger.1.html | ||||||
|  |     prefix_ue: 'apache_plain_' | ||||||
|  |     prefix_ssl: 'apache_ssl_' | ||||||
|  | 
 | ||||||
|  |   user: 'www-data' | ||||||
|  |   group: 'www-data' | ||||||
|  | 
 | ||||||
|  |   # additions to the main apache config | ||||||
|  |   config:  # see: https://httpd.apache.org/docs/2.4/mod/core.html | ||||||
|  |     ServerTokens: 'Prod' | ||||||
|  |     ServerSignature: 'Off' | ||||||
|  |     FileETag: 'None' | ||||||
|  |     KeepAlive: 'On' | ||||||
|  |     KeepAliveTimeout: 5 | ||||||
|  |     MaxKeepAliveRequests: 100 | ||||||
|  |     LimitRequestBody: 51200000  # 50MB => if you use file-uploads you might need to change this (0=unlimited, max=2147483647 [2GB]) | ||||||
|  |     LimitRequestFields: 50 | ||||||
|  |     LimitRequestFieldSize: 8190 | ||||||
|  |     LimitRequestLine: 8190 | ||||||
|  |     LimitXMLRequestBody: 1000000 | ||||||
|  |     TimeOut: 60 | ||||||
|  |     TraceEnable: 'off' | ||||||
|  |     # ssl option => see: https://httpd.apache.org/docs/2.4/mod/mod_ssl.html | ||||||
|  |     SSLProtocol: 'ALL -TLSv1.1 -TLSv1 -SSLv2 -SSLv3' | ||||||
|  |     SSLCipherSuite: 'ALL:+HIGH:!ADH:!EXP:!SSLv2:!SSLv3:!MEDIUM:!LOW:!NULL:!aNULL' | ||||||
|  |     SSLHonorCipherOrder: 'on' | ||||||
|  |     SSLOptions: '+StrictRequire' | ||||||
|  |     SSLSessionTickets: 'off' | ||||||
|  |     SSLCompression: 'off' | ||||||
|  | 
 | ||||||
|  |   headers:  # https://htaccessbook.com/important-security-headers/ | https://geekflare.com/http-header-implementation/ | ||||||
|  |     'Header always set Strict-Transport-Security': '"max-age=31536000; includeSubDomains; preload"' | ||||||
|  |     'Referrer-Policy': '"same-origin"' | ||||||
|  |     'Content-Security-Policy': "\"default-src 'self';\"" | ||||||
|  |     'X-Frame-Options': 'SAMEORIGIN' | ||||||
|  |     'X-Content-Type-Options': 'nosniff' | ||||||
|  |     'X-Permitted-Cross-Domain-Policies': '"none"' | ||||||
|  |     'X-XSS-Protection': '"1; mode=block"' | ||||||
|  |     'Header always edit Set-Cookie ^(.*)$': '$1;HttpOnly;Secure;SameSite=None' | ||||||
|  |     # 'Header set Permissions-Policy': '"none"' | ||||||
|  |     # 'Header set Content-Security-Policy': '"default-src https:; font-src https:; img-src https:; script-src https:; style-src https:;"' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   modules: | ||||||
|  |     present: ['ssl', 'headers', 'rewrite'] | ||||||
|  |     absent: ['autoindex'] | ||||||
|  | 
 | ||||||
|  |   letsencrypt: | ||||||
|  |     key_size: 4096 | ||||||
|  |     path: '/etc/letsencrypt' | ||||||
|  |     path_key: '/etc/ssl/private' | ||||||
|  |     path_cert: '/etc/ssl/certs' | ||||||
|  |     renew_timer: 'Mon *-*-* 00:00:00' | ||||||
|  |     verbosity: 'v' | ||||||
|  | 
 | ||||||
|  | APACHE_CONFIG: "{{ default_apache | combine(apache, recursive=true) }}" | ||||||
|  | 
 | ||||||
|  | # site-specific config | ||||||
|  | default_site_config: | ||||||
|  |   mode: 'serve' | ||||||
|  |   admin: 'apache@template.ansibleguy.net' | ||||||
|  |   port_plain: 80 | ||||||
|  |   port_ssl: 443 | ||||||
|  | 
 | ||||||
|  |   config: {}  # site-specific setting-value pairs | ||||||
|  |   config_additions: []  # lines that will 1-to-1 be appended to the site-config | ||||||
|  | 
 | ||||||
|  |   security:  # https://www.nixpal.com/apache-httpd-hardening/ | ||||||
|  |     disable_root_index: true | ||||||
|  |     disable_directory_access: true | ||||||
|  |     disable_ssi_cgi: true | ||||||
|  |     limit_directory_access: true | ||||||
|  | 
 | ||||||
|  |   redirect: | ||||||
|  |     target: 'https://github.com/ansibleguy' | ||||||
|  |     request_uri: true | ||||||
|  | 
 | ||||||
|  |   serve: | ||||||
|  |     path: '/var/www/html' | ||||||
|  | 
 | ||||||
|  |   ssl: | ||||||
|  |     mode: 'letsencrypt'  # local/selfsigned/letsencrypt | ||||||
|  |     file_pub: '/etc/apache2/ssl/DOMAIN.crt'  # should use the certificate chain => top is server cert; bottom root cert | ||||||
|  |     file_key: '/etc/apache2/ssl/DOMAIN.key' | ||||||
|  |     file_csr: '/etc/apache2/ssl/DOMAIN.csr' | ||||||
|  |     file_ca: | ||||||
|  |     csr_data: | ||||||
|  |       country: 'AT' | ||||||
|  |       org: 'AnsibleGuy' | ||||||
|  |       email: 'apache@template.ansibleguy.net' | ||||||
|  |       cn: 'Apache Certificate' | ||||||
|  | 
 | ||||||
|  | default_modules: | ||||||
|  |   # <IfModule ${MOD}> | ||||||
|  |   # </IfModule> | ||||||
|  |   prefork: # see: https://httpd.apache.org/docs/2.4/mod/mpm_common.html | ||||||
|  |     ifname: 'prefork.c' | ||||||
|  |     settings: | ||||||
|  |       StartServers: 5 | ||||||
|  |       MinSpareServers: 5 | ||||||
|  |       MaxSpareServers: 10 | ||||||
|  |       MaxRequestWorkers: 256 | ||||||
|  |       MaxConnectionsPerChild: 0 | ||||||
|  |   mod_evasive: | ||||||
|  |     ifname: 'mod_evasive20.c' | ||||||
|  |     settings: | ||||||
|  |       DOSHashTableSize: 4096 | ||||||
|  |       DOSPageCount: 25 | ||||||
|  |       DOSSiteCount: 100 | ||||||
|  |       DOSPageInterval: 1 | ||||||
|  |       DOSSiteInterval: 1 | ||||||
|  |       DOSBlockingPeriod: 60 | ||||||
|  |       DOSLogDir: "{{ CONFIG.log.path }}" | ||||||
|  |       # DOSSystemCommand: | ||||||
|  |       # DOSEmailNotify: mail@yourdomain.com | ||||||
|  |       DOSWhitelist: [ | ||||||
|  |           '127.0.0.*', '192.168.*.*', '10.*.*.*', '172.16.*.*', '172.17.*.*', '172.18.*.*', '172.19.*.*', | ||||||
|  |           '172.20.*.*', '172.21.*.*', '172.22.*.*', '172.23.*.*', '172.24.*.*', '172.25.*.*', '172.26.*.*', | ||||||
|  |           '172.27.*.*', '172.28.*.*', '172.29.*.*', '172.30.*.*', '172.31.*.*', '172.32.*.*', | ||||||
|  |       ] | ||||||
|  | 
 | ||||||
|  | APACHE_MODULES: "{{ default_modules | combine(modules, recursive=true) }}" | ||||||
|  | 
 | ||||||
|  | packages: | ||||||
|  |   apache: ['apache2'] | ||||||
|  |   letsencrypt: ['python3-certbot-apache'] | ||||||
|  | 
 | ||||||
|  | apache_config_graylist: [ | ||||||
|  |     'SSLEngine', 'SSLCertificateKeyFile', 'SSLCertificateFile', 'SSLCertificateChainFile', 'ErrorLog', 'CustomLog', 'ServerAdmin', | ||||||
|  |     'ServerAlias', 'ServerName', 'Redirect' | ||||||
|  | ] | ||||||
|  | apache_restricted_methods: ['GET', 'POST', 'HEAD'] | ||||||
|  | @ -0,0 +1,18 @@ | ||||||
|  | from re import sub as regex_replace | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class FilterModule(object): | ||||||
|  | 
 | ||||||
|  |     def filters(self): | ||||||
|  |         return { | ||||||
|  |             "safe_key": self.safe_key, | ||||||
|  |             "all_true": self.all_true, | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def safe_key(key: str) -> str: | ||||||
|  |         return regex_replace('[^0-9a-zA-Z]+', '', key.replace(' ', '_')) | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def all_true(data: list) -> bool: | ||||||
|  |         return all(data) | ||||||
|  | @ -0,0 +1,23 @@ | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | galaxy_info: | ||||||
|  |   author: 'AnsibleGuy <guy@ansibleguy.net>' | ||||||
|  |   readme: 'README.md' | ||||||
|  |   license: 'MIT' | ||||||
|  |   repository: 'https://github.com/ansibleguy/infra_apache' | ||||||
|  |   issue_tracker_url: 'https://github.com/ansibleguy/infra_apache/issues' | ||||||
|  |   github_branch: 'stable' | ||||||
|  |   min_ansible_version: 2.9.0 | ||||||
|  |   description: 'Role to deploy apache2 sites on a linux server' | ||||||
|  |   platforms: | ||||||
|  |     - name: Debian | ||||||
|  |       versions: | ||||||
|  |         - bullseye | ||||||
|  |   galaxy_tags: | ||||||
|  |     - 'web' | ||||||
|  |     - 'webserver' | ||||||
|  |     - 'apache' | ||||||
|  | 
 | ||||||
|  | collections: | ||||||
|  |   - 'community.crypto' | ||||||
|  |   - 'community.general' | ||||||
|  | @ -0,0 +1,9 @@ | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | # ansible-playbook -K -D -i inventory/hosts.yml playbook.yml | ||||||
|  | 
 | ||||||
|  | - hosts: all   # should be limited to web-servers | ||||||
|  |   become: true | ||||||
|  |   gather_facts: yes | ||||||
|  |   roles: | ||||||
|  |   - ansibleguy.infra_apache | ||||||
|  | @ -0,0 +1,9 @@ | ||||||
|  | # external roles and collections to download | ||||||
|  | # install: ansible-galaxy install -r requirements.yml | ||||||
|  | 
 | ||||||
|  | collections: | ||||||
|  |   - name: 'community.crypto' | ||||||
|  |     source: 'https://galaxy.ansible.com' | ||||||
|  | 
 | ||||||
|  |   - name: 'community.general' | ||||||
|  |     source: 'https://galaxy.ansible.com' | ||||||
|  | @ -0,0 +1,68 @@ | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | - name: "Apache | Debian | Config | Site '{{ name }}' | Configuring listen-ports" | ||||||
|  |   ansible.builtin.blockinfile: | ||||||
|  |     path: '/etc/apache2/ports.conf' | ||||||
|  |     block: | | ||||||
|  |       Listen {{ port }} | ||||||
|  |     marker: "# {mark} ANSIBLE MANAGED BLOCK - port '{{ port }}'" | ||||||
|  |     insertafter: '# /etc/apache2/sites-enabled/000-default.conf' | ||||||
|  |   ignore_errors: true | ||||||
|  |   when: | ||||||
|  |     - port != 80 | ||||||
|  |     - port != 443 | ||||||
|  |     - port != '80' | ||||||
|  |     - port != '443' | ||||||
|  |   loop_control: | ||||||
|  |     loop_var: port | ||||||
|  |   with_items: | ||||||
|  |     - "{{ site.port_plain }}" | ||||||
|  |     - "{{ site.port_ssl }}" | ||||||
|  | 
 | ||||||
|  | - name: "Apache | Debian | Config | Site '{{ name }}' | Create root directory" | ||||||
|  |   ansible.builtin.file: | ||||||
|  |     path: "{{ site.serve.path }}" | ||||||
|  |     state: directory | ||||||
|  |     owner: "{{ APACHE_CONFIG.user }}" | ||||||
|  |     group: "{{ APACHE_CONFIG.group }}" | ||||||
|  |     mode: 0755 | ||||||
|  |   when: site.mode == 'serve' | ||||||
|  | 
 | ||||||
|  | - name: "Apache | Debian | Config | Site '{{ name }}' | Configuring site" | ||||||
|  |   ansible.builtin.template: | ||||||
|  |     src: 'templates/etc/apache2/sites-available/site.conf.j2' | ||||||
|  |     dest: "/etc/apache2/sites-available/site_{{ name }}.conf" | ||||||
|  |     owner: 'root' | ||||||
|  |     group: 'root' | ||||||
|  |     mode: 0644 | ||||||
|  |     validate: 'apachectl -t -f %s' | ||||||
|  |   register: apache_config_deployment | ||||||
|  |   ignore_errors: yes | ||||||
|  | 
 | ||||||
|  | - name: "Apache | Debian | Config | Site '{{ name }}' | Ask user" | ||||||
|  |   ansible.builtin.pause: | ||||||
|  |     prompt: "The apache config validation failed! Sometimes this is a false-negative. | ||||||
|  |     Do you want to force the deployment? (yes/no)" | ||||||
|  |   register: force_deploy | ||||||
|  |   when: apache_config_deployment.failed | ||||||
|  | 
 | ||||||
|  | - name: "Apache | Debian | Config | Site '{{ name }}' | Configuring site (forced)" | ||||||
|  |   ansible.builtin.template: | ||||||
|  |     src: 'templates/etc/apache2/sites-available/site.conf.j2' | ||||||
|  |     dest: "/etc/apache2/sites-available/site_{{ name }}.conf" | ||||||
|  |     owner: 'root' | ||||||
|  |     group: 'root' | ||||||
|  |     mode: 0644 | ||||||
|  |     backup: true | ||||||
|  |   when: | ||||||
|  |     - apache_config_deployment.failed | ||||||
|  |     - force_deploy.user_input == 'yes' | ||||||
|  | 
 | ||||||
|  | - name: "Apache | Debian | Config | Site '{{ name }}' | Enabling site" | ||||||
|  |   ansible.builtin.file: | ||||||
|  |     state: link | ||||||
|  |     src: "/etc/apache2/sites-available/site_{{ name }}.conf" | ||||||
|  |     dest: "/etc/apache2/sites-enabled/site_{{ name }}.conf" | ||||||
|  |     owner: 'root' | ||||||
|  |     group: 'root' | ||||||
|  |     mode: 0644 | ||||||
|  | @ -0,0 +1,13 @@ | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | LetsEncrypt Certbot | Cleanup | Disable temporary apache site | ||||||
|  |   ansible.builtin.file: | ||||||
|  |     state: absent | ||||||
|  |     dest: '/etc/apache2/sites-enabled/tmp_le_dummy.conf' | ||||||
|  |   register: tmp_site_config | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | LetsEncrypt Certbot | Cleanup | Reload apache | ||||||
|  |   ansible.builtin.systemd: | ||||||
|  |     name: 'apache2.service' | ||||||
|  |     state: reloaded | ||||||
|  |   when: tmp_site_config.changed | ||||||
|  | @ -0,0 +1,23 @@ | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | LetsEncrypt Certbot | Dependencies | Deploying temporary apache site | ||||||
|  |   ansible.builtin.template: | ||||||
|  |     src: 'templates/etc/apache2/sites-available/le_dummy.conf.j2' | ||||||
|  |     dest: '/etc/apache2/sites-available/tmp_le_dummy.conf' | ||||||
|  |     owner: 'root' | ||||||
|  |     group: 'root' | ||||||
|  |     mode: 0644 | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | LetsEncrypt Certbot | Dependencies | Enable apache site | ||||||
|  |   ansible.builtin.file: | ||||||
|  |     state: link | ||||||
|  |     src: '/etc/apache2/sites-available/tmp_le_dummy.conf' | ||||||
|  |     dest: '/etc/apache2/sites-enabled/tmp_le_dummy.conf' | ||||||
|  |     owner: 'root' | ||||||
|  |     group: 'root' | ||||||
|  |     mode: 0644 | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | LetsEncrypt Certbot | Dependencies | Reload apache | ||||||
|  |   ansible.builtin.systemd: | ||||||
|  |     name: 'apache2.service' | ||||||
|  |     state: reloaded | ||||||
|  | @ -0,0 +1,45 @@ | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | - name: "Apache | Debian | LetsEncrypt Certbot | Checking if cert for domain '{{ site.domain }}' exists" | ||||||
|  |   ansible.builtin.shell: 'certbot certificates' | ||||||
|  |   register: domain_cert | ||||||
|  |   changed_when: false | ||||||
|  | 
 | ||||||
|  | # todo: check domains registered in current certificate (certbot certificates) and remove it if there are more than configured before re-configuring it | ||||||
|  | 
 | ||||||
|  | - name: "Apache | Debian | LetsEncrypt Certbot | Set key/cert paths for domain '{{ site.domain }}'" | ||||||
|  |   ansible.builtin.set_fact: | ||||||
|  |     _path_key: "{{ APACHE_CONFIG.letsencrypt.path_key }}/{{ name }}" | ||||||
|  |     _path_cert: "{{ APACHE_CONFIG.letsencrypt.path_cert }}/{{ name }}" | ||||||
|  |     _path_live: "{{ APACHE_CONFIG.letsencrypt.path }}/live/{{ name }}" | ||||||
|  | 
 | ||||||
|  | - name: "Apache | Debian | LetsEncrypt Certbot | Creating key/cert directories for domain '{{ site.domain }}'" | ||||||
|  |   ansible.builtin.file: | ||||||
|  |     path: "{{ item }}" | ||||||
|  |     state: directory | ||||||
|  |     owner: 'root' | ||||||
|  |     group: 'root' | ||||||
|  |     mode: 0755 | ||||||
|  |   with_items: | ||||||
|  |   - "{{ _path_key }}" | ||||||
|  |   - "{{ _path_cert }}" | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | LetsEncrypt Certbot | Getting cert | ||||||
|  |   ansible.builtin.include_tasks: domain_new.yml | ||||||
|  |   when: domain_cert.stdout.find(site.domain) == -1 | ||||||
|  | 
 | ||||||
|  | - name: "Apache | Debian | LetsEncrypt Certbot | Linking certificates for domain '{{ site.domain }}'" | ||||||
|  |   ansible.builtin.file: | ||||||
|  |     state: link | ||||||
|  |     src: "{{ item.value.src }}" | ||||||
|  |     dest: "{{ item.value.dst }}" | ||||||
|  |     owner: "{{ APACHE_CONFIG.user }}" | ||||||
|  |     group: "{{ APACHE_CONFIG.group }}" | ||||||
|  |     mode: 0400 | ||||||
|  |     follow: yes | ||||||
|  |   with_dict: | ||||||
|  |     - {'config': {'dst': "{{ _path_key }}/privkey.pem", 'src': "{{ _path_live }}/privkey.pem"}} | ||||||
|  |     - {'config': {'dst': "{{ _path_cert }}/cert.pem", 'src': "{{ _path_live }}/cert.pem"}} | ||||||
|  |     - {'config': {'dst': "{{ _path_cert }}/chain.pem", 'src': "{{ _path_live }}/chain.pem"}} | ||||||
|  |     - {'config': {'dst': "{{ _path_cert }}/fullchain.pem", 'src': "{{ _path_live }}/fullchain.pem"}} | ||||||
|  |   ignore_errors: yes | ||||||
|  | @ -0,0 +1,26 @@ | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | - name: "Apache | Debian | LetsEncrypt Certbot | Creating alternative name string (1/3)" | ||||||
|  |   ansible.builtin.set_fact: | ||||||
|  |     _aliases: "{{ site.aliases | join(' --domain ') }}" | ||||||
|  |   when: apache_aliases | length > 0 | ||||||
|  | 
 | ||||||
|  | - name: "Apache | Debian | LetsEncrypt Certbot | Creating alternative name string (2/3)" | ||||||
|  |   ansible.builtin.set_fact: | ||||||
|  |     _apache_aliases: "{{ '--domain ' + _aliases }}" | ||||||
|  |   when: apache_aliases | length > 0 | ||||||
|  | 
 | ||||||
|  | - name: "Apache | Debian | LetsEncrypt Certbot | Creating alternative name string (3/3)" | ||||||
|  |   ansible.builtin.set_fact: | ||||||
|  |     _apache_aliases: '' | ||||||
|  |   when: apache_aliases | length == 0 | ||||||
|  | 
 | ||||||
|  | - name: debug | ||||||
|  |   ansible.builtin.debug: | ||||||
|  |     msg: "certbot certonly --apache -{{ APACHE_CONFIG.letsencrypt.verbosity }} --non-interactive --agree-tos --email {{ site.admin }} --cert-name {{ name }} | ||||||
|  |     --rsa-key-size {{ APACHE_CONFIG.letsencrypt.key_size }} --no-redirect --domain {{ site.domain }} {{ _apache_aliases }}" | ||||||
|  | 
 | ||||||
|  | - name: "Apache | Debian | LetsEncrypt Certbot | Starting certbot for domain '{{ site.domain }}'" | ||||||
|  |   ansible.builtin.shell: "certbot certonly --apache -{{ APACHE_CONFIG.letsencrypt.verbosity }} --non-interactive --agree-tos --email {{ site.admin }} --cert-name {{ name }} | ||||||
|  |   --rsa-key-size {{ APACHE_CONFIG.letsencrypt.key_size }} --no-redirect --domain {{ site.domain }} {{ _apache_aliases }}" | ||||||
|  |   ignore_errors: yes | ||||||
|  | @ -0,0 +1,41 @@ | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | LetsEncrypt Certbot | Install package | ||||||
|  |   ansible.builtin.apt: | ||||||
|  |     name: "{{ packages.letsencrypt }}" | ||||||
|  |     state: present | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | LetsEncrypt Certbot | Check if a apache virtualhost is available | ||||||
|  |   ansible.builtin.shell: 'ls /etc/apache2/sites-enabled/' | ||||||
|  |   register: enabled_apache_sites | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | LetsEncrypt Certbot | Checking dependencies | ||||||
|  |   ansible.builtin.include_tasks: dependencies.yml | ||||||
|  |   when: enabled_apache_sites.stdout == '' | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | LetsEncrypt Certbot | Processing apache sites | ||||||
|  |   ansible.builtin.include_tasks: domain.yml | ||||||
|  |   vars: | ||||||
|  |     site: "{{ default_site_config | combine(site_item, recursive=true) }}" | ||||||
|  |     name: "{{ site_item.key | safe_key }}" | ||||||
|  |   loop_control: | ||||||
|  |     loop_var: site_item | ||||||
|  |   with_dict: "{{ APACHE_CONFIG.sites }}" | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | LetsEncrypt Certbot | Cleanup dependencies | ||||||
|  |   ansible.builtin.include_tasks: cleanup.yml | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | LetsEncrypt Certbot | Adding systemd files for certbot renewal | ||||||
|  |   ansible.builtin.template: | ||||||
|  |     src: "templates/etc/systemd/system/{{ item }}.j2" | ||||||
|  |     dest: "/etc/systemd/system/{{ item }}" | ||||||
|  |   with_items: | ||||||
|  |     - 'ansibleguy.infra_apache.LetsEncryptCertbot.service' | ||||||
|  |     - 'ansibleguy.infra_apache.LetsEncryptCertbot.timer' | ||||||
|  | 
 | ||||||
|  | - name:  Apache | Debian | LetsEncrypt Certbot | Enabling cert-renewal systemd timer | ||||||
|  |   ansible.builtin.systemd: | ||||||
|  |     daemon_reload: yes | ||||||
|  |     name: 'LetsEncryptCertbot.timer' | ||||||
|  |     enabled: yes | ||||||
|  |     state: started | ||||||
|  | @ -0,0 +1,100 @@ | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | Install apache | ||||||
|  |   ansible.builtin.apt: | ||||||
|  |     name: "{{ packages.apache }}" | ||||||
|  |     state: present | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | Checking if all sites exist (1/2) | ||||||
|  |   ansible.builtin.stat: | ||||||
|  |     path: "/etc/apache2/sites-available/site_{{ item.key | safe_key }}.conf" | ||||||
|  |   register: sites_exist_raw | ||||||
|  |   with_dict: "{{ APACHE_CONFIG.sites }}" | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | Checking if all sites exist (2/2) | ||||||
|  |   ansible.builtin.set_fact: | ||||||
|  |     sites_exist: "{{ sites_exist_raw | json_query('[*].results.stat.exists') | all_true }}" | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | Getting certificate via LetsEncrypt | ||||||
|  |   ansible.builtin.import_tasks: letsencrypt/main.yml | ||||||
|  |   when: > | ||||||
|  |     (APACHE_CONFIG.ssl.renew or | ||||||
|  |     not sites_exist) and | ||||||
|  |     APACHE_CONFIG.ssl.mode == 'letsencrypt' | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | Enabling apache modules | ||||||
|  |   community.general.apache2_module: | ||||||
|  |     state: present | ||||||
|  |     name: "{{ item }}" | ||||||
|  |   when: item not in APACHE_CONFIG.modules.absent | ||||||
|  |   loop: "{{ APACHE_CONFIG.modules.present }}" | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | Disabling apache modules | ||||||
|  |   community.general.apache2_module: | ||||||
|  |     state: absent | ||||||
|  |     name: "{{ item }}" | ||||||
|  |   loop: "{{ APACHE_CONFIG.modules.absent }}" | ||||||
|  | 
 | ||||||
|  | # todo: configure module settings | ||||||
|  | 
 | ||||||
|  | # todo: check if apache2.conf editing is still needed | ||||||
|  | #- name: Apache | Debian | Adding global config | ||||||
|  | #  ansible.builtin.blockinfile: | ||||||
|  | #    path: '/etc/apache2/apache2.conf' | ||||||
|  | #    block: | | ||||||
|  | #      {% for setting, value in apache_config_additions_default.items() %} | ||||||
|  | #        {{ setting }} {{ value }} | ||||||
|  | #      {% endfor %} | ||||||
|  | #      {% for setting, value in apache_config_additions.items() %} | ||||||
|  | #        {{ setting }} {{ value }} | ||||||
|  | #      {% endfor %} | ||||||
|  | #    marker: "# {mark} ANSIBLE MANAGED BLOCK - global config" | ||||||
|  | #    validate: 'apachectl -t -f %s' | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | Disabling default apache sites | ||||||
|  |   ansible.builtin.file: | ||||||
|  |     state: absent | ||||||
|  |     dest: "/etc/apache2/sites-enabled/{{ item }}" | ||||||
|  |   with_items: | ||||||
|  |     - '000-default.conf' | ||||||
|  |     - 'default-ssl.conf' | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | Removing apache site | ||||||
|  |   ansible.builtin.include_tasks: rm_site.yml | ||||||
|  |   vars: | ||||||
|  |     site: "{{ default_site_config | combine(site_item, recursive=true) }}" | ||||||
|  |     name: "{{ site_item.key | safe_key }}" | ||||||
|  |   when: site_item.state | default('present') != 'present' | ||||||
|  |   loop_control: | ||||||
|  |     loop_var: site_item | ||||||
|  |   with_dict: "{{ APACHE_CONFIG.sites }}" | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | Reloading apache | ||||||
|  |   ansible.builtin.systemd: | ||||||
|  |     name: 'apache2.service' | ||||||
|  |     state: reloaded | ||||||
|  |   tags: [base, config, sites, certs] | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | Adding apache site | ||||||
|  |   ansible.builtin.include_tasks: add_site.yml | ||||||
|  |   vars: | ||||||
|  |     site: "{{ default_site_config | combine(site_item, recursive=true) }}" | ||||||
|  |     name: "{{ site_item.key | safe_key }}" | ||||||
|  |   when: site_item.state | default('present') == 'present' | ||||||
|  |   loop_control: | ||||||
|  |     loop_var: site_item | ||||||
|  |   with_dict: "{{ APACHE_CONFIG.sites }}" | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | Starting/Enabling apache | ||||||
|  |   ansible.builtin.systemd: | ||||||
|  |     name: 'apache2.service' | ||||||
|  |     enabled: yes | ||||||
|  |     state: started | ||||||
|  |   tags: [base] | ||||||
|  | 
 | ||||||
|  | - name: Apache | Debian | Reloading apache | ||||||
|  |   ansible.builtin.systemd: | ||||||
|  |     name: 'apache2.service' | ||||||
|  |     enabled: yes | ||||||
|  |     state: reloaded | ||||||
|  |   tags: [base, config, sites, certs] | ||||||
|  | @ -0,0 +1,24 @@ | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | # ports will be left configured since I found no clean way to manage them statefully | ||||||
|  | 
 | ||||||
|  | - name: "Apache | Debian | Config | Site '{{ name }}' | Removing web-root" | ||||||
|  |   ansible.builtin.file: | ||||||
|  |     path: "{{ site.serve.path }}" | ||||||
|  |     state: absent | ||||||
|  |     force: yes | ||||||
|  |   when: site.mode == 'serve' | ||||||
|  | 
 | ||||||
|  | - name: "Apache | Debian | Config | Site '{{ name }}' | Removing/Disabling site" | ||||||
|  |   ansible.builtin.template: | ||||||
|  |     path: "{{ item }}" | ||||||
|  |     state: absent | ||||||
|  |   loop: | ||||||
|  |     - "/etc/apache2/sites-available/site_{{ name }}.conf" | ||||||
|  |     - "/etc/apache2/sites-enabled/site_{{ name }}.conf" | ||||||
|  | 
 | ||||||
|  | - name: "Apache | Debian | Config | Site '{{ name }}' | Removing certificate from certbot" | ||||||
|  |   ansible.builtin.shell: "certbot certonly --apache -{{ APACHE_LE_CONFIG.verbosity }} --non-interactive --agree-tos --email {{ site.admin }} --cert-name {{ name }} | ||||||
|  |   --rsa-key-size {{ APACHE_LE_CONFIG.key_size }} --no-redirect --domain {{ site.domain }} {{ _apache_aliases }}" | ||||||
|  |   ignore_errors: yes | ||||||
|  |   when: site.ssl.mode == 'letsencrypt' | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | - name: Apache | Processing debian config | ||||||
|  |   ansible.builtin.import_tasks: debian/main.yml | ||||||
|  |   when: "ansible_distribution|lower in ['debian', 'ubuntu']" | ||||||
|  | @ -0,0 +1,6 @@ | ||||||
|  | <VirtualHost *:80> | ||||||
|  |     ServerName dummy.letsencrypt.localhost | ||||||
|  |     ServerAdmin webmaster@localhost | ||||||
|  |     ErrorLog {{ APACHE_CONFIG.log.path }}/error.log | ||||||
|  |     CustomLog {{ APACHE_CONFIG.log.path }}/access.log combined | ||||||
|  | </VirtualHost> | ||||||
|  | @ -0,0 +1,163 @@ | ||||||
|  | <VirtualHost *:{{ site.port_plain }}> | ||||||
|  |   ServerName {{ site.domain }} | ||||||
|  | 
 | ||||||
|  | {% if site.aliases | length > 0 %} | ||||||
|  |   ServerAlias {% for name in site.aliases %} {{ name }} {% endfor %} | ||||||
|  | {% endif %} | ||||||
|  |   ServerAdmin {{ site.admin }} | ||||||
|  | 
 | ||||||
|  |   # log config | ||||||
|  | {% if APACHE_CONFIG.log.syslog and APACHE_CONFIG.log.syslog_host is not none %} | ||||||
|  |   ErrorLog "| /usr/bin/logger -n {{ APACHE_CONFIG.log.syslog_host }} -P {{ APACHE_CONFIG.log.syslog_port }} -p local1.error -t {{ APACHE_CONFIG.log.prefix_ue }}{{ name }}" | ||||||
|  |   CustomLog "| /usr/bin/logger -n {{ APACHE_CONFIG.log.syslog_host }} -P {{ APACHE_CONFIG.log.syslog_port }} -p local1.info -t {{ APACHE_CONFIG.log.prefix_ue }}{{ name }}" combined | ||||||
|  | {% elif APACHE_CONFIG.log.syslog %} | ||||||
|  |   ErrorLog "| /usr/bin/logger -p local1.error -t {{ APACHE_CONFIG.log.prefix_ue }}{{ name }}" | ||||||
|  |   CustomLog "| /usr/bin/logger -p local1.info -t {{ APACHE_CONFIG.log.prefix_ue }}{{ name }}" combined | ||||||
|  | {% elif APACHE_CONFIG.log.per_site %} | ||||||
|  |   ErrorLog {{ APACHE_CONFIG.log.path }}/{{ name }}_error.log | ||||||
|  |   CustomLog {{ APACHE_CONFIG.log.path }}/{{ name }}_access.log combined | ||||||
|  | {% else %} | ||||||
|  |   ErrorLog {{ APACHE_CONFIG.log.path }}/error.log | ||||||
|  |   CustomLog {{ APACHE_CONFIG.log.path }}/access.log combined | ||||||
|  | {% endif %} | ||||||
|  | 
 | ||||||
|  |   # redirect all to secure connection | ||||||
|  |   Redirect permanent / https://{{ site.domain }} | ||||||
|  | 
 | ||||||
|  | </VirtualHost> | ||||||
|  | 
 | ||||||
|  | <VirtualHost *:{{ site.port_ssl }}> | ||||||
|  |   ServerName {{ site.domain }} | ||||||
|  | 
 | ||||||
|  | {% if site.aliases | length > 0 %} | ||||||
|  |   ServerAlias {% for name in site.aliases %} {{ name }} {% endfor %} | ||||||
|  | {% endif %} | ||||||
|  | 
 | ||||||
|  |   ServerAdmin {{ site.admin }} | ||||||
|  | 
 | ||||||
|  |   # log config | ||||||
|  | {% if APACHE_CONFIG.log.syslog and APACHE_CONFIG.log.syslog_host is not none %} | ||||||
|  |   ErrorLog "| /usr/bin/logger -n {{ APACHE_CONFIG.log.syslog_host }} -P {{ APACHE_CONFIG.log.syslog_port }} -p local1.error -t {{ APACHE_CONFIG.log.prefix_ue }}{{ name }}" | ||||||
|  |   CustomLog "| /usr/bin/logger -n {{ APACHE_CONFIG.log.syslog_host }} -P {{ APACHE_CONFIG.log.syslog_port }} -p local1.info -t {{ APACHE_CONFIG.log.prefix_ue }}{{ name }}" combined | ||||||
|  | {% elif APACHE_CONFIG.log.syslog %} | ||||||
|  |   ErrorLog "| /usr/bin/logger -p local1.error -t {{ APACHE_CONFIG.log.prefix_ue }}{{ name }}" | ||||||
|  |   CustomLog "| /usr/bin/logger -p local1.info -t {{ APACHE_CONFIG.log.prefix_ue }}{{ name }}" combined | ||||||
|  | {% elif APACHE_CONFIG.log.per_site %} | ||||||
|  |   ErrorLog {{ APACHE_CONFIG.log.path }}/{{ name }}_error.log | ||||||
|  |   CustomLog {{ APACHE_CONFIG.log.path }}/{{ name }}_access.log combined | ||||||
|  | {% else %} | ||||||
|  |   ErrorLog {{ APACHE_CONFIG.log.path }}/error.log | ||||||
|  |   CustomLog {{ APACHE_CONFIG.log.path }}/access.log combined | ||||||
|  | {% endif %} | ||||||
|  | 
 | ||||||
|  |   # ssl config | ||||||
|  |   <IfModule mod_ssl.c> | ||||||
|  |     SSLEngine on | ||||||
|  |     SSLCertificateKeyFile /etc/ssl/private/{{ apache_site }}/privkey.pem | ||||||
|  |     SSLCertificateFile /etc/ssl/certs/{{ apache_site }}/cert.pem | ||||||
|  |     SSLCertificateChainFile /etc/ssl/certs/{{ apache_site }}/fullchain.pem | ||||||
|  |   </IfModule> | ||||||
|  | 
 | ||||||
|  | {% if APACHE_CONFIG.config | length > 0 %} | ||||||
|  |   # global config | ||||||
|  | {% for setting, value in APACHE_CONFIG.config.items() %} | ||||||
|  | {%   if setting not in apache_config_graylist %} | ||||||
|  |   {{ setting }} {{ value }} | ||||||
|  | {%   endif %} | ||||||
|  | {% endfor %} | ||||||
|  | {% endif %} | ||||||
|  | 
 | ||||||
|  | {% if site.config | length > 0 %} | ||||||
|  |   # site-specific config | ||||||
|  | {% for setting, value in site.config.items() %} | ||||||
|  | {%   if setting not in apache_config_graylist %} | ||||||
|  |   {{ setting }} {{ value }} | ||||||
|  | {%   endif %} | ||||||
|  | {% endfor %} | ||||||
|  | {% endif %} | ||||||
|  | 
 | ||||||
|  | {% if APACHE_CONFIG.headers | length > 0 %} | ||||||
|  |   # global headers | ||||||
|  |   <IfModule mod_headers.c> | ||||||
|  | {%   for header, value in APACHE_CONFIG.headers.items() %} | ||||||
|  | {%     if 'Header' in header %} | ||||||
|  |     {{ header }} {{ value }} | ||||||
|  | {%     else %} | ||||||
|  |     Header set {{ header }} {{ value }} | ||||||
|  | {%     endif %} | ||||||
|  | {%   endfor %} | ||||||
|  | 
 | ||||||
|  |   </IfModule> | ||||||
|  | {% endif %} | ||||||
|  | 
 | ||||||
|  | {% if site.headers | length > 0 %} | ||||||
|  |   # site-specific headers | ||||||
|  |   <IfModule mod_headers.c> | ||||||
|  | {%   for header, value in site.headers.items() %} | ||||||
|  | {%     if 'Header' in header %} | ||||||
|  |     {{ header }} {{ value }} | ||||||
|  | {%     else %} | ||||||
|  |     Header set {{ header }} {{ value }} | ||||||
|  | {%     endif %} | ||||||
|  | {%   endfor %} | ||||||
|  | 
 | ||||||
|  |   </IfModule> | ||||||
|  | {% endif %} | ||||||
|  | 
 | ||||||
|  |   # security config | ||||||
|  | {% if site.security.restrict_methods %} | ||||||
|  |   <LimitExcept {% for method in apache_restricted_methods %}{{ method }} {% endfor %}> | ||||||
|  |     deny from all | ||||||
|  |   </LimitExcept> | ||||||
|  | {% endif %} | ||||||
|  | {% if site.security.limit_directory_access %} | ||||||
|  |   <Directory /> | ||||||
|  |     Options None | ||||||
|  |     Order deny,allow | ||||||
|  |     Deny from all | ||||||
|  |   </Directory> | ||||||
|  | {% endif %} | ||||||
|  | {% if site.security.disable_directory_access %} | ||||||
|  |   <Directory "="> | ||||||
|  |     Require all denied | ||||||
|  |   </Directory> | ||||||
|  | {% endif %} | ||||||
|  | 
 | ||||||
|  | {% if site.mode == 'redirect' %} | ||||||
|  |   # redirect-mode config | ||||||
|  |   Redirect permanent / {{ site.redirect.target }} | ||||||
|  | {%   if site.redirect.request_uri %} | ||||||
|  |   RedirectMatch permanent ^/(.*)$ {{ site.redirect.target }}/$1 | ||||||
|  | {%   else %} | ||||||
|  |   RedirectMatch permanent ^/(.*)$ {{ site.redirect.target }} | ||||||
|  | {%   endif %} | ||||||
|  | 
 | ||||||
|  | {% elif site.mode == 'serve' %} | ||||||
|  |   # serve-mode config | ||||||
|  |   DocumentRoot {{ site.serve.path }} | ||||||
|  | 
 | ||||||
|  |   # mode-specific security config | ||||||
|  |   <Directory {{ site.serve.path }}> | ||||||
|  | {%   if site.security.disable_ssi_cgi %} | ||||||
|  |     Options -FollowSymLinks -ExecCGI -Includes | ||||||
|  |     AllowOverride None | ||||||
|  |     Require all granted | ||||||
|  | {%   endif %} | ||||||
|  | {%   if site.security.disable_root_index %} | ||||||
|  |     Options -Indexes | ||||||
|  | {%   endif %} | ||||||
|  |   </Directory> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | {% endif %} | ||||||
|  | 
 | ||||||
|  | {% if site.config_additions | length > 0 %} | ||||||
|  |   # additional lines | ||||||
|  | {% endif %} | ||||||
|  | {% for line in site.config_additions %} | ||||||
|  |   {{ line }} | ||||||
|  | {% endfor %} | ||||||
|  | 
 | ||||||
|  | </VirtualHost> | ||||||
|  | 
 | ||||||
|  | ServerName {{ site.domain }} | ||||||
|  | @ -0,0 +1,7 @@ | ||||||
|  | [Unit] | ||||||
|  | Description=Service to renew LetsEncrypt Certificates using certbot | ||||||
|  | 
 | ||||||
|  | [Service] | ||||||
|  | Type=oneshot | ||||||
|  | ExecStart=certbot renew -{{ APACHE_CONFIG.letsencrypt.verbosity }} --non-interactive --agree-tos --renew-with-new-domains --rsa-key-size {{ APACHE_CONFIG.letsencrypt.key_size }} | ||||||
|  | SuccessExitStatus=0 | ||||||
|  | @ -0,0 +1,10 @@ | ||||||
|  | [Unit] | ||||||
|  | Description=Timer to renew LetsEncrypt Certificates using certbot | ||||||
|  | 
 | ||||||
|  | [Timer] | ||||||
|  | OnCalendar={{ APACHE_CONFIG.letsencrypt.renew_timer }} | ||||||
|  | Persistent=false | ||||||
|  | WakeSystem=false | ||||||
|  | 
 | ||||||
|  | [Install] | ||||||
|  | WantedBy=multi-user.target | ||||||
		Loading…
	
		Reference in New Issue