From b0f520c8b55a24c595a0dfc77e6fce4a6279e215 Mon Sep 17 00:00:00 2001 From: AnsibleGuy Date: Thu, 4 Nov 2021 22:06:08 +0100 Subject: [PATCH] created role to configure multiple apache sites, using ansibleguy.infra_certs role to generate certificates --- README.md | 104 ++++++++++++--- defaults/main.yml | 77 ++++++++--- filter_plugins/utils.py | 16 ++- requirements.yml | 5 + tasks/debian/add_certs.yml | 67 ++++++++++ tasks/debian/add_site.yml | 46 +++---- tasks/debian/letsencrypt/cleanup.yml | 13 -- tasks/debian/letsencrypt/dependencies.yml | 23 ---- tasks/debian/letsencrypt/domain.yml | 45 ------- tasks/debian/letsencrypt/domain_new.yml | 26 ---- tasks/debian/letsencrypt/main.yml | 41 ------ tasks/debian/main.yml | 121 +++++++++++++----- tasks/debian/rm_site.yml | 22 +--- tasks/main.yml | 9 ++ .../apache2/sites-available/le_dummy.conf.j2 | 6 - .../etc/apache2/sites-available/site.conf.j2 | 49 +++---- 16 files changed, 379 insertions(+), 291 deletions(-) create mode 100644 tasks/debian/add_certs.yml delete mode 100644 tasks/debian/letsencrypt/cleanup.yml delete mode 100644 tasks/debian/letsencrypt/dependencies.yml delete mode 100644 tasks/debian/letsencrypt/domain.yml delete mode 100644 tasks/debian/letsencrypt/domain_new.yml delete mode 100644 tasks/debian/letsencrypt/main.yml delete mode 100644 templates/etc/apache2/sites-available/le_dummy.conf.j2 diff --git a/README.md b/README.md index d2d5859..7e8e15f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Apache2 Ansible Role +# Apache2 Role Ansible role to install apache2 sites on the target server. **Tested:** @@ -6,17 +6,44 @@ Ansible role to install apache2 sites on the target server. ## Functionality -* Package installation +* **Package installation** * Ansible dependencies (_minimal_) * Apache2 -* Configuration - * - * Default opt-in: - * - * Default opt-outs: - * - * Default config: - * + + +* **Configuration** + * Support for multiple sites/servers + * Two **config-modes**: + * serve (_default_) + * redirect + + + * **Default config**: + * Disabled: Generate self-signed ones + * **ca** => Generate a minimal Certificate Authority and certificate signed by it + * **letsencrypt** => Uses the LetsEncrypt certbot + * **existing** => Copy certificate files or use existing ones + + + * **Default opt-ins**: + * restricting methods to POST/GET/HEAD + + + * **Default opt-outs**: + * Include the config file 'site_{{ site_name }}_app.conf' for advanced usage + + +Options to provide module config will be added in the future!
+Also some basic mods will get a pre-config added. (_prefork, evasive_) ## Info @@ -25,24 +52,65 @@ Ansible role to install apache2 sites on the target server. * **Note:** this role currently only supports debian-based systems + +* **Note:** This role expects that the site's unencrypted 'server' will only redirect to its encrypted connection. + + +* **Note:** If you want all domain-names to get 'caught' by a site/server you need to add an underline '*' as alias or domain!
+This will also be done automatically if no domain is supplied. + ## Requirements -* Community collection: ```ansible-galaxy install -r requirements.yml``` +* Community collection and certificate role: ```ansible-galaxy install -r requirements.yml``` ## Usage + +### Config + +Define the apache dictionary as needed! + +```yaml +apache: + headers: + mySuperCustom: 'headerContent' + + modules: + present: ['evasive'] + + guys_statics: + mode: 'serve' + domain: 'static.guy.net' + serve: + path: '/var/www/static' + + ssl: + mode: 'ca' # create minimal ca with signed server-certificate + + config: + KeepAliveTimeout: 10 + + git_stuff: + mode: 'redirect' + domain: 'ansibleguy.net' + aliases: ['www.ansibleguy.net'] + redirect: + target: 'https://github.com/ansibleguy' + + ssl: + mode: 'letsencrypt' + + letsencrypt: + email: 'apache@template.ansibleguy.net' +``` + +### Execution + 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 diff --git a/defaults/main.yml b/defaults/main.yml index d1bbf04..b78e9a4 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -21,10 +21,13 @@ default_apache: user: 'www-data' group: 'www-data' - # additions to the main apache config - config: # see: https://httpd.apache.org/docs/2.4/mod/core.html + settings: + # setting to be set in apache2.conf ServerTokens: 'Prod' ServerSignature: 'Off' + + # additions to the main apache config + config: # see: https://httpd.apache.org/docs/2.4/mod/core.html FileETag: 'None' KeepAlive: 'On' KeepAliveTimeout: 5 @@ -45,6 +48,7 @@ default_apache: SSLCompression: 'off' headers: # https://htaccessbook.com/important-security-headers/ | https://geekflare.com/http-header-implementation/ + # if first key does not include 'Header' => prepend 'Header set' 'Header always set Strict-Transport-Security': '"max-age=31536000; includeSubDomains; preload"' 'Referrer-Policy': '"same-origin"' 'Content-Security-Policy': "\"default-src 'self';\"" @@ -56,6 +60,18 @@ default_apache: # '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:;"' + ssl: + path: '/etc/apache2/ssl' + ca: + file: # can be used if you want to use an existing ca + cn: 'Apache CA Certificate' + org: 'AnsibleGuy' + ou: + country: + state: + locality: + email: + pwd: # it's highly recommended setting a passphrase! modules: present: ['ssl', 'headers', 'rewrite'] @@ -64,28 +80,32 @@ default_apache: letsencrypt: key_size: 4096 path: '/etc/letsencrypt' - path_key: '/etc/ssl/private' - path_cert: '/etc/ssl/certs' - renew_timer: 'Mon *-*-* 00:00:00' + renew_timer: 'Mon *-*-* 03:00:00' verbosity: 'v' + email: + renew: false # if a renewal should be started by the role; the renewal service will auto-renew the certificates otherwise APACHE_CONFIG: "{{ default_apache | combine(apache, recursive=true) }}" # site-specific config default_site_config: mode: 'serve' + state: 'present' admin: 'apache@template.ansibleguy.net' port_plain: 80 port_ssl: 443 + aliases: [] + ip: config: {} # site-specific setting-value pairs config_additions: [] # lines that will 1-to-1 be appended to the site-config + app_include: false + headers: {} security: # https://www.nixpal.com/apache-httpd-hardening/ disable_root_index: true - disable_directory_access: true disable_ssi_cgi: true - limit_directory_access: true + restrict_methods: true redirect: target: 'https://github.com/ansibleguy' @@ -95,16 +115,37 @@ default_site_config: 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' + mode: 'selfsigned' # existing/selfsigned/ca/letsencrypt + # existing: + # We expect the certs to be placed in the role's 'files' directory named like the site + # Example: files/certs/ansibleguy.key and files/certs/ansibleguy.crt + # letsencrypt: + # Host needs to have a valid public dns record pointed at it + # Needs to be publicly reachable over port 80/tcp + cert: + name: cn: 'Apache Certificate' + org: 'AnsibleGuy' + ou: + country: + state: + locality: + email: + crl_distribution: [] + ca: + file: # can be used if you want to use an existing ca + cn: + org: + ou: + country: + state: + locality: + email: + pwd: # it's highly recommended setting a passphrase! + + letsencrypt: + key_size: + email: default_modules: # @@ -137,10 +178,6 @@ default_modules: 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' diff --git a/filter_plugins/utils.py b/filter_plugins/utils.py index 67971d1..c957ca6 100644 --- a/filter_plugins/utils.py +++ b/filter_plugins/utils.py @@ -7,12 +7,26 @@ class FilterModule(object): return { "safe_key": self.safe_key, "all_true": self.all_true, + "prepare_letsencrypt": self.prepare_letsencrypt, } @staticmethod def safe_key(key: str) -> str: - return regex_replace('[^0-9a-zA-Z]+', '', key.replace(' ', '_')) + return regex_replace(r'[^0-9a-zA-Z\.]+', '', key.replace(' ', '_')) @staticmethod def all_true(data: list) -> bool: return all(data) + + @staticmethod + def prepare_letsencrypt(site: dict, name: str) -> dict: + domains = [site['domain']] + domains.extend(site['aliases']) + return { + name: { + 'domains': domains, + 'key_size': site['letsencrypt']['key_size'], + 'email': site['letsencrypt']['email'], + 'state': site['state'], + } + } diff --git a/requirements.yml b/requirements.yml index 3cfb7b6..6ee4e2a 100644 --- a/requirements.yml +++ b/requirements.yml @@ -7,3 +7,8 @@ collections: - name: 'community.general' source: 'https://galaxy.ansible.com' + +roles: + - src: 'https://github.com/ansibleguy/infra_certs.git' + version: 'stable' + name: 'ansibleguy.infra_certs' diff --git a/tasks/debian/add_certs.yml b/tasks/debian/add_certs.yml new file mode 100644 index 0000000..92ded11 --- /dev/null +++ b/tasks/debian/add_certs.yml @@ -0,0 +1,67 @@ +--- + +- name: "Apache | Debian | Site '{{ name }}' | Certs | Creating public directory" + ansible.builtin.file: + path: "{{ APACHE_CONFIG.ssl.path }}" + state: directory + mode: 0755 + +- name: "Apache | Debian | Site '{{ name }}' | Certs | Creating certificates" + ansible.builtin.import_role: + name: ansibleguy.infra_certs + vars: + certs: + mode: "{{ site.ssl.mode }}" + path: "{{ APACHE_CONFIG.ssl.path }}" + owner_key: "{{ APACHE_CONFIG.user }}" + group_key: "{{ APACHE_CONFIG.group }}" + owner_cert: "{{ APACHE_CONFIG.user }}" + group_cert: "{{ APACHE_CONFIG.group }}" + cert: + name: "{{ name }}" + cn: "{{ site.ssl.cert.cn }}" + org: "{{ site.ssl.cert.org }}" + ou: "{{ site.ssl.cert.ou }}" + country: "{{ site.ssl.cert.country }}" + state: "{{ site.ssl.cert.state }}" + locality: "{{ site.ssl.cert.locality }}" + email: "{{ site.ssl.cert.email }}" + crl_distribution: "{{ site.ssl.cert.crl_distribution }}" + domains: "{{ site.aliases + [site.domain] }}" + ips: ["{{ site.ip }}"] + ca: + path: "{{ APACHE_CONFIG.ssl.path }}" + cn: "{{ site.ssl.ca.cn | default(APACHE_CONFIG.ssl.ca.cn, true) }}" + org: "{{ site.ssl.ca.org | default(APACHE_CONFIG.ssl.ca.org, true) }}" + ou: "{{ site.ssl.ca.ou | default(APACHE_CONFIG.ssl.ca.ou, true) }}" + country: "{{ site.ssl.ca.country | default(APACHE_CONFIG.ssl.ca.country, true) }}" + state: "{{ site.ssl.ca.state | default(APACHE_CONFIG.ssl.ca.state, true) }}" + locality: "{{ site.ssl.ca.locality | default(APACHE_CONFIG.ssl.ca.locality, true) }}" + email: "{{ site.ssl.ca.email | default(APACHE_CONFIG.ssl.ca.email, true) }}" + pwd: "{{ site.ssl.ca.pwd | default(APACHE_CONFIG.ssl.ca.pwd, true) }}" + when: "site.ssl.mode in ['ca', 'selfsigned']" + +- name: "Apache | Debian | Site '{{ name }}' | Certs | Trying to copy cert pub" + ansible.builtin.copy: + dest: "{{ APACHE_CONFIG.ssl.path }}/{{ name }}.crt" + src: "files/certs/{{ name }}.crt" + mode: 0644 + owner: "{{ APACHE_CONFIG.user }}" + group: "{{ APACHE_CONFIG.group }}" + ignore_errors: true + register: copy_cert_pub + when: site.ssl.mode == 'existing' + +- name: "Apache | Debian | Site '{{ name }}' | Certs | Trying to copy cert pk" + ansible.builtin.copy: + dest: "{{ APACHE_CONFIG.ssl.path }}/{{ name }}.key" + src: "files/certs/{{ name }}.key" + mode: 0640 + owner: "{{ APACHE_CONFIG.user }}" + group: "{{ APACHE_CONFIG.group }}" + no_log: true + register: copy_cert_key + ignore_errors: true + when: + - site.ssl.mode == 'existing' + - copy_cert_pub.failed is undefined or not copy_cert_pub.failed diff --git a/tasks/debian/add_site.yml b/tasks/debian/add_site.yml index 5448f5f..0f08fd9 100644 --- a/tasks/debian/add_site.yml +++ b/tasks/debian/add_site.yml @@ -1,6 +1,18 @@ --- -- name: "Apache | Debian | Config | Site '{{ name }}' | Configuring listen-ports" +- name: "Apache | Debian | Site '{{ name }}' | Checking config" + ansible.builtin.fail: + msg: "The required site-configuration was not provided! + Needed: 'domain'" + when: site.domain is undefined + tags: [config, sites, certs] + +- name: "Apache | Debian | Site '{{ name }}' | Configuring certificates" + ansible.builtin.import_tasks: add_certs.yml + when: "site.ssl.mode in ['selfsigned', 'existing', 'ca']" + tags: [sites, certs] + +- name: "Apache | Debian | Site '{{ name }}' | Configuring listen-ports" ansible.builtin.blockinfile: path: '/etc/apache2/ports.conf' block: | @@ -18,8 +30,9 @@ with_items: - "{{ site.port_plain }}" - "{{ site.port_ssl }}" + tags: [config, sites] -- name: "Apache | Debian | Config | Site '{{ name }}' | Create root directory" +- name: "Apache | Debian | Site '{{ name }}' | Create root directory" ansible.builtin.file: path: "{{ site.serve.path }}" state: directory @@ -27,38 +40,18 @@ group: "{{ APACHE_CONFIG.group }}" mode: 0755 when: site.mode == 'serve' + tags: [sites] -- name: "Apache | Debian | Config | Site '{{ name }}' | Configuring site" +- name: "Apache | Debian | 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 + tags: [config, sites] -- 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" +- name: "Apache | Debian | Site '{{ name }}' | Enabling site" ansible.builtin.file: state: link src: "/etc/apache2/sites-available/site_{{ name }}.conf" @@ -66,3 +59,4 @@ owner: 'root' group: 'root' mode: 0644 + tags: [sites] diff --git a/tasks/debian/letsencrypt/cleanup.yml b/tasks/debian/letsencrypt/cleanup.yml deleted file mode 100644 index 239ccc2..0000000 --- a/tasks/debian/letsencrypt/cleanup.yml +++ /dev/null @@ -1,13 +0,0 @@ ---- - -- 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 diff --git a/tasks/debian/letsencrypt/dependencies.yml b/tasks/debian/letsencrypt/dependencies.yml deleted file mode 100644 index bf562cc..0000000 --- a/tasks/debian/letsencrypt/dependencies.yml +++ /dev/null @@ -1,23 +0,0 @@ ---- - -- 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 diff --git a/tasks/debian/letsencrypt/domain.yml b/tasks/debian/letsencrypt/domain.yml deleted file mode 100644 index c1a3006..0000000 --- a/tasks/debian/letsencrypt/domain.yml +++ /dev/null @@ -1,45 +0,0 @@ ---- - -- 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 diff --git a/tasks/debian/letsencrypt/domain_new.yml b/tasks/debian/letsencrypt/domain_new.yml deleted file mode 100644 index ad3a373..0000000 --- a/tasks/debian/letsencrypt/domain_new.yml +++ /dev/null @@ -1,26 +0,0 @@ ---- - -- 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 diff --git a/tasks/debian/letsencrypt/main.yml b/tasks/debian/letsencrypt/main.yml deleted file mode 100644 index d243781..0000000 --- a/tasks/debian/letsencrypt/main.yml +++ /dev/null @@ -1,41 +0,0 @@ ---- - -- 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 diff --git a/tasks/debian/main.yml b/tasks/debian/main.yml index e327e95..d311211 100644 --- a/tasks/debian/main.yml +++ b/tasks/debian/main.yml @@ -2,54 +2,101 @@ - name: Apache | Debian | Install apache ansible.builtin.apt: - name: "{{ packages.apache }}" + name: ['apache2'] state: present + update_cache: true + tags: [base] -- 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 | Creating service user + ansible.builtin.user: + name: "{{ APACHE_CONFIG.user }}" + shell: '/usr/sbin/nologin' + comment: 'Apache Service User' + tags: [base] -- 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 | Setting service user + ansible.builtin.lineinfile: + state: present + path: '/etc/apache2/envvars' + regexp: "{{ item.reg }}" + line: "{{ item.line }}" + register: apache_user_update_raw + loop: + - {reg: '^export APACHE_RUN_USER=', line: "export APACHE_RUN_USER={{ APACHE_CONFIG.user }}"} + - {reg: '^export APACHE_RUN_GROUP=', line: "export APACHE_RUN_GROUP={{ APACHE_CONFIG.group }}"} + tags: [base, config] - name: Apache | Debian | Enabling apache modules community.general.apache2_module: state: present name: "{{ item }}" when: item not in APACHE_CONFIG.modules.absent + register: apache_mods_enable_raw loop: "{{ APACHE_CONFIG.modules.present }}" + tags: [base] - name: Apache | Debian | Disabling apache modules community.general.apache2_module: state: absent name: "{{ item }}" + force: True + ignore_configcheck: True + register: apache_mods_disable_raw loop: "{{ APACHE_CONFIG.modules.absent }}" + tags: [base] # 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 | Adding main settings + ansible.builtin.lineinfile: + state: present + path: '/etc/apache2/apache2.conf' + regexp: "{{ item.key }}\\s" + line: "{{ item.key }} {{ item.value }}" + validate: "apachectl -t -f %s" + register: apache_settings_raw + with_dict: "{{ APACHE_CONFIG.settings }}" + tags: [config, base] + +- name: Apache | Debian | Restarting apache + ansible.builtin.systemd: + name: 'apache2.service' + state: restarted + when: > + apache_user_update_raw.changed or + apache_mods_enable_raw.changed or + apache_mods_disable_raw.changed or + apache_settings_raw.changed + tags: [base, config] + +# is an additional site-loop since certificates can be pre-/absent +- name: Apache | Debian | Getting certificates using LetsEncrypt + ansible.builtin.include_role: + name: ansibleguy.infra_certs + when: site.ssl.mode == 'letsencrypt' + vars: + site: "{{ default_site_config | combine(site_item.value, recursive=true) }}" + name: "{{ site_item.key | safe_key }}" + certs: + mode: 'le_certbot' + path: "{{ APACHE_CONFIG.ssl.path }}" + owner_key: "{{ APACHE_CONFIG.user }}" + group_key: "{{ APACHE_CONFIG.group }}" + owner_cert: "{{ APACHE_CONFIG.user }}" + group_cert: "{{ APACHE_CONFIG.group }}" + letsencrypt: + certs: "{{ site | prepare_letsencrypt(name) }}" + path: "{{ APACHE_CONFIG.letsencrypt.path }}" + email: "{{ APACHE_CONFIG.letsencrypt.email }}" + renew_timer: "{{ APACHE_CONFIG.letsencrypt.renew_timer }}" + verbosity: "{{ APACHE_CONFIG.letsencrypt.verbosity }}" + service: 'apache' + renew: "{{ APACHE_CONFIG.letsencrypt.renew }}" + loop_control: + loop_var: site_item + with_dict: "{{ APACHE_CONFIG.sites }}" + no_log: true + tags: [certs, sites] - name: Apache | Debian | Disabling default apache sites ansible.builtin.file: @@ -58,16 +105,19 @@ with_items: - '000-default.conf' - 'default-ssl.conf' + tags: [config, base] -- name: Apache | Debian | Removing apache site +- name: Apache | Debian | Removing site ansible.builtin.include_tasks: rm_site.yml + when: site.state != 'present' vars: - site: "{{ default_site_config | combine(site_item, recursive=true) }}" + site: "{{ default_site_config | combine(site_item.value, 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 }}" + no_log: true + tags: [config, sites, certs] - name: Apache | Debian | Reloading apache ansible.builtin.systemd: @@ -75,15 +125,16 @@ state: reloaded tags: [base, config, sites, certs] -- name: Apache | Debian | Adding apache site +- name: Apache | Debian | Adding site ansible.builtin.include_tasks: add_site.yml + when: site.state == 'present' vars: - site: "{{ default_site_config | combine(site_item, recursive=true) }}" + site: "{{ default_site_config | combine(site_item.value, 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 }}" + tags: [config, sites, certs] - name: Apache | Debian | Starting/Enabling apache ansible.builtin.systemd: diff --git a/tasks/debian/rm_site.yml b/tasks/debian/rm_site.yml index 9baf434..87d9883 100644 --- a/tasks/debian/rm_site.yml +++ b/tasks/debian/rm_site.yml @@ -1,24 +1,16 @@ --- # 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' +# also: the web-root will be left as-is - name: "Apache | Debian | Config | Site '{{ name }}' | Removing/Disabling site" - ansible.builtin.template: + ansible.builtin.file: 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' + - "/etc/apache2/sites-available/site_{{ name }}.conf" + - "{{ APACHE_CONFIG.ssl.path }}/{{ name }}.key" + - "{{ APACHE_CONFIG.ssl.path }}/{{ name }}.crt" + - "{{ APACHE_CONFIG.ssl.path }}/{{ name }}.chain.crt" + - "{{ APACHE_CONFIG.ssl.path }}/{{ name }}.fullchain.crt" diff --git a/tasks/main.yml b/tasks/main.yml index 4fc8048..f77e460 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,5 +1,14 @@ --- +- name: Apache | Checking config + ansible.builtin.fail: + msg: "The required configuration was not provided! + Needed: 'apache', 'apache.sites'" + when: > + apache is undefined or + apache.sites is undefined or + apache.sites | length == 0 + - name: Apache | Processing debian config ansible.builtin.import_tasks: debian/main.yml when: "ansible_distribution|lower in ['debian', 'ubuntu']" diff --git a/templates/etc/apache2/sites-available/le_dummy.conf.j2 b/templates/etc/apache2/sites-available/le_dummy.conf.j2 deleted file mode 100644 index 4f47676..0000000 --- a/templates/etc/apache2/sites-available/le_dummy.conf.j2 +++ /dev/null @@ -1,6 +0,0 @@ - - ServerName dummy.letsencrypt.localhost - ServerAdmin webmaster@localhost - ErrorLog {{ APACHE_CONFIG.log.path }}/error.log - CustomLog {{ APACHE_CONFIG.log.path }}/access.log combined - diff --git a/templates/etc/apache2/sites-available/site.conf.j2 b/templates/etc/apache2/sites-available/site.conf.j2 index 3ca4008..da96cf0 100644 --- a/templates/etc/apache2/sites-available/site.conf.j2 +++ b/templates/etc/apache2/sites-available/site.conf.j2 @@ -1,8 +1,11 @@ +# {{ ansible_managed }} +# ansibleguy.infra_apache + ServerName {{ site.domain }} {% if site.aliases | length > 0 %} - ServerAlias {% for name in site.aliases %} {{ name }} {% endfor %} + ServerAlias {% for name in site.aliases %} {{ name }} {% endfor %}{% if site.ip is not none %} {{ site.ip }}{% endif %} {% endif %} ServerAdmin {{ site.admin }} @@ -30,18 +33,18 @@ ServerName {{ site.domain }} {% if site.aliases | length > 0 %} - ServerAlias {% for name in site.aliases %} {{ name }} {% endfor %} + ServerAlias {% for alias in site.aliases %} {{ alias }} {% endfor %}{% if site.ip is not none %} {{ site.ip }}{% endif %} {% 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 + ErrorLog "| /usr/bin/logger -n {{ APACHE_CONFIG.log.syslog_host }} -P {{ APACHE_CONFIG.log.syslog_port }} -p local1.error -t {{ APACHE_CONFIG.log.prefix_ssl }}{{ 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_ssl }}{{ 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 + ErrorLog "| /usr/bin/logger -p local1.error -t {{ APACHE_CONFIG.log.prefix_ssl }}{{ name }}" + CustomLog "| /usr/bin/logger -p local1.info -t {{ APACHE_CONFIG.log.prefix_ssl }}{{ 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 @@ -53,9 +56,11 @@ # ssl config 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 + SSLCertificateKeyFile {{ APACHE_CONFIG.ssl.path }}/{{ name }}.key + SSLCertificateFile {{ APACHE_CONFIG.ssl.path }}/{{ name }}.crt +{% if site.ssl.mode != 'selfsigned' %} + SSLCertificateChainFile {{ APACHE_CONFIG.ssl.path }}/{{ name }}{% if site.ssl.mode == 'letsencrypt' %}.fullchain{% else %}.chain{% endif %}.crt +{% endif %} {% if APACHE_CONFIG.config | length > 0 %} @@ -106,20 +111,15 @@ # security config {% if site.security.restrict_methods %} - - deny from all - -{% endif %} -{% if site.security.limit_directory_access %} + + RewriteEngine On + RewriteCond %{REQUEST_METHOD} ^(?!{% for method in apache_restricted_methods %}{{ method }}{% if not loop.last %}|{% endif %}{% endfor %}) + RewriteRule .* - [F] + - Options None - Order deny,allow - Deny from all - -{% endif %} -{% if site.security.disable_directory_access %} -