From 0987ee1cba78f7a13b26085da93426619d6fd071 Mon Sep 17 00:00:00 2001 From: Alain Takoudjou <alain.takoudjou@nexedi.com> Date: Wed, 28 Jun 2017 11:55:50 +0200 Subject: [PATCH] stack.erp5: Intergrate caucase and client-certificate-based authentication Allows requesting a caucase partition and reusing an existing caucase instance. For client-certificate-based authentication, client must be able to access backend directly (frontend is not possible). --- component/apache/apache-backend.conf.in | 20 ++++- component/apache/buildout.cfg | 2 +- software/erp5/instance-erp5-input-schema.json | 28 +++++++ .../erp5/instance-erp5-output-schema.json | 5 ++ stack/erp5/buildout.cfg | 5 ++ stack/erp5/buildout.hash.cfg | 6 +- stack/erp5/instance-balancer.cfg.in | 79 ++++++++++++++++++- stack/erp5/instance-erp5.cfg.in | 22 +++++- stack/erp5/instance.cfg.in | 6 +- 9 files changed, 162 insertions(+), 11 deletions(-) diff --git a/component/apache/apache-backend.conf.in b/component/apache/apache-backend.conf.in index 02bfe3877..65daaa8c3 100644 --- a/component/apache/apache-backend.conf.in +++ b/component/apache/apache-backend.conf.in @@ -108,12 +108,16 @@ SSLProxyEngine On # As backend is trusting REMOTE_USER header unset it always RequestHeader unset REMOTE_USER +RequestHeader unset SSL_CLIENT_SERIAL {% if parameter_dict['ca-cert'] -%} -SSLVerifyClient require +SSLVerifyClient optional RequestHeader set REMOTE_USER %{SSL_CLIENT_S_DN_CN}s +RequestHeader set SSL_CLIENT_SERIAL "%{SSL_CLIENT_M_SERIAL}s" SSLCACertificateFile {{ parameter_dict['ca-cert'] }} +{% if parameter_dict['crl'] -%} SSLCARevocationCheck chain SSLCARevocationFile {{ parameter_dict['crl'] }} +{%- endif %} {%- endif %} ErrorLog "{{ parameter_dict['error-log'] }}" @@ -128,12 +132,24 @@ CustomLog "{{ parameter_dict['access-log'] }}" combined </Directory> RewriteEngine On -{% for port, _, backend in parameter_dict['backend-list'] -%} +{% for port, _, backend, enable_authentication in parameter_dict['backend-list'] -%} {% for ip in parameter_dict['ip-list'] -%} Listen {{ ip }}:{{ port }} {% endfor -%} <VirtualHost *:{{ port }}> SSLEngine on +{% if enable_authentication and parameter_dict['ca-cert'] and parameter_dict['crl'] -%} + SSLVerifyClient require + SSLCACertificateFile {{ parameter_dict['ca-cert'] }} + SSLCARevocationCheck chain + SSLCARevocationFile {{ parameter_dict['crl'] }} + + LogFormat "%h %l %{REMOTE_USER}i %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %D" combined + + # We would like to separate the the authentificated logs. + ErrorLog "{{ parameter_dict['log-dir'] }}/apache-service-error.log" + CustomLog "{{ parameter_dict['log-dir'] }}/apache-service-access.log" combined +{% endif -%} RewriteRule ^/(.*) {{ backend }}/$1 [L,P] </VirtualHost> {% endfor -%} diff --git a/component/apache/buildout.cfg b/component/apache/buildout.cfg index 7c01e7ec4..fa7238829 100644 --- a/component/apache/buildout.cfg +++ b/component/apache/buildout.cfg @@ -190,5 +190,5 @@ make-targets = [template-apache-backend-conf] recipe = slapos.recipe.build:download url = ${:_profile_base_location_}/apache-backend.conf.in -md5sum = feef079241bda3407b7ceed5876cb61f +md5sum = 8f1f92ad308ab6bad973c790ee583428 mode = 640 diff --git a/software/erp5/instance-erp5-input-schema.json b/software/erp5/instance-erp5-input-schema.json index a0b410f81..9f873be66 100644 --- a/software/erp5/instance-erp5-input-schema.json +++ b/software/erp5/instance-erp5-input-schema.json @@ -111,6 +111,12 @@ "default": 5, "type": "integer" }, + "ssl-authentication": { + "title": "Enable SSL Client authentication on this zope instance.", + "description": "If set to true, will set SSL Client verification to required on apache VirtualHost which allow to access this zope instance.", + "type": "boolean", + "default": false + }, "webdav": { "description": "Serve webdav queries, implies timerserver-interval=0 (disabled). Mixing webdav and non-webdav nodes in a single family will give unspecified results.", "default": false, @@ -263,6 +269,28 @@ "description": "In wendelin.core there are 2 formats for storing data, so called ZBlk0 and ZBlk1. See https://lab.nexedi.com/nexedi/wendelin.core/blob/2e5e1d3d/bigfile/file_zodb.py#L19 for more details.", "default": "", "type": "string" + }, + "caucase": { + "description": "Caucase certificate authority parameters", + "properties": { + "url": { + "title": "Caucase URL", + "description": "URL of existing caucase instance to use. If empty, a new caucase instance will be deployed. If not empty, other properties in this section will be ignored.", + "default": "", + "type": "string", + "format": "uri" + }, + "crl-update-periodicity": { + "title": "Periodicity of CRL update", + "description": "Periodicity of CRL update, in cron format. The CRL will be downloaded from caucase URL and the new content will be saved if there was a change. Everytime a new CRL is writen, Apache reload will be called.", + "type": "string", + "default": "0 0 * * *" + } + }, + "additionalProperties": { + "$ref": "../caucase/instance-caucase-input-schema.json#/properties" + }, + "type": "object" } } } diff --git a/software/erp5/instance-erp5-output-schema.json b/software/erp5/instance-erp5-output-schema.json index 20a9d720d..22e60374c 100644 --- a/software/erp5/instance-erp5-output-schema.json +++ b/software/erp5/instance-erp5-output-schema.json @@ -72,6 +72,11 @@ "description": "Jupyter notebook web UI access information", "pattern": "^https://", "type": "string" + }, + "caucase-http-url": { + "description": "Caucase url on HTTP. For HTTPS URL, uses https scheme, if port is explicitely specified in http URL, take that port and add 1 and use it as https port. If it is not specified.", + "pattern": "^http://", + "type": "string" } }, "patternProperties": { diff --git a/stack/erp5/buildout.cfg b/stack/erp5/buildout.cfg index 4c150488f..0aac734c3 100644 --- a/stack/erp5/buildout.cfg +++ b/stack/erp5/buildout.cfg @@ -62,6 +62,7 @@ extends = ../../component/postfix/buildout.cfg ../monitor/buildout.cfg ../../software/ipython_notebook/software.cfg + ../../software/caucase/software.cfg ../../software/neoppod/software-common.cfg # keep neoppod extends last @@ -149,6 +150,9 @@ extra-ldflags = -Wl,-rpath=${gcc:location}/lib -Wl,-rpath=${gcc:location}/lib64 [instance-jupyter] rendered = ${buildout:directory}/template-jupyter.cfg +[instance-caucase] +rendered = ${buildout:directory}/instance-caucase.cfg + [download-base] <= download-base-neo url = ${:_profile_base_location_}/${:filename} @@ -231,6 +235,7 @@ context = key bin_directory buildout:bin-directory key buildout_bin_directory buildout:bin-directory key cairo_location cairo:location + key caucase_template instance-caucase:rendered key coreutils_location coreutils:location key cups_location cups:location key curl_location curl:location diff --git a/stack/erp5/buildout.hash.cfg b/stack/erp5/buildout.hash.cfg index 5287c23c6..7166a8ef4 100644 --- a/stack/erp5/buildout.hash.cfg +++ b/stack/erp5/buildout.hash.cfg @@ -75,7 +75,7 @@ md5sum = 0969fbb25b05c02ef3c2d437b2f4e1a0 [template] filename = instance.cfg.in -md5sum = 09862389b2bf3da3f9387a6424329da9 +md5sum = e364ea67bfe786b6b6ebd6c4f0cd628a [monitor-template-dummy] filename = dummy.cfg @@ -83,7 +83,7 @@ md5sum = d41d8cd98f00b204e9800998ecf8427e [template-erp5] filename = instance-erp5.cfg.in -md5sum = 929b87c01aaf6ea1bbf6ff906ce9c0d9 +md5sum = 13638031b6b6c9ad9c0a9c4e6d9a202a [template-zeo] filename = instance-zeo.cfg.in @@ -95,7 +95,7 @@ md5sum = 6a64d1615c3ef9f6311c863d5aa0c58f [template-balancer] filename = instance-balancer.cfg.in -md5sum = a3ad32c46bb56076895441edbd66018d +md5sum = f2fb0c537c124622fe8e89afe0188519 [template-haproxy-cfg] filename = haproxy.cfg.in diff --git a/stack/erp5/instance-balancer.cfg.in b/stack/erp5/instance-balancer.cfg.in index 1aab5c5fe..25bb53ce0 100644 --- a/stack/erp5/instance-balancer.cfg.in +++ b/stack/erp5/instance-balancer.cfg.in @@ -1,5 +1,6 @@ {% set part_list = [] -%} {% set ssl_parameter_dict = slapparameter_dict.get('ssl', {}) %} +{% set caucase_url = slapparameter_dict.get('caucase-url', '') -%} {% macro section(name) %}{% do part_list.append(name) %}{{ name }}{% endmacro -%} {% set use_ipv6 = slapparameter_dict.get('use-ipv6', False) -%} {# @@ -36,6 +37,56 @@ context = key content {{content_section_name}}:content mode = {{ mode }} {%- endmacro %} +[certificate-request-base] +recipe = slapos.cookbook:wrapper +wrapper-path = ${directory:bin}/request-instance-certificate +parameters-extra = true +command-line = {{ parameter_dict['bin-directory'] }}/caucase-cliweb + --crt-file ${apache-conf-ssl:cert} + --key-file ${apache-conf-ssl:key} + --crl-file ${apache-conf-ssl:crl} + --ca-url {{ caucase_url }} + --ca-crt-file ${apache-conf-ssl:ca-cert} + +{% macro request_cert(name, common_name) -%} +{% set get_crl_periodicity = slapparameter_dict.get('crl-update-periodicity', 'daily') -%} + +[{{ section(name ~ '-certificate-request') }}] +recipe = slapos.cookbook:wrapper +wrapper-path = ${directory:services}/request-{{ name }}-certificate +command-line = + ${certificate-request-base:wrapper-path} + --cn {{ common_name }} + --request + +[{{ section(name ~ '-renew-cron-entry') }}] +recipe = slapos.cookbook:cron.d +cron-entries = ${cron:cron-entries} +name = {{ name }}-certificate-auto-renew +time = weekly +# 2592000 = 30*24*60*60 equivalent to one month in seconds +command = ${certificate-request-base:wrapper-path} --renew --threshold 2592000 --on-renew="${apache-graceful:output}" + +[{{ section(name ~ '-download-crl') }}] +# download the crl for the first time +recipe = plone.recipe.command +command = + if [ ! -s "${apache-conf-ssl:crl}" ]; then + ${certificate-request-base:wrapper-path} --update-crl + fi +update-command = ${:command} +stop-on-error = true + +[{{ section(name ~ '-update-crl-cron-entry') }}] +recipe = slapos.cookbook:cron.d +cron-entries = ${cron:cron-entries} +name = {{ name }}-update-crl +time = {{ get_crl_periodicity }} +# XXX - Update crl call apache graceful restart, it's not recommended to check crl too often, Apache +# has an issue with reload and can be frozen and stop responding. Default periodicity time = daily +command = ${certificate-request-base:wrapper-path} --update-crl --on-crl-update="${apache-graceful:output}" +{%- endmacro %} + {% if use_ipv6 -%} [zope-tunnel-base] recipe = slapos.cookbook:ipv4toipv6 @@ -81,6 +132,7 @@ ipv6 = {{ zope_address.split(']:')[0][1:] }} -#} {% do zope_family_address_list[0][0] -%} {% set haproxy_port = next_port() -%} +{% set backend_path = slapparameter_dict['backend-path-dict'][family_name] -%} {% do haproxy_dict.__setitem__(family_name, (haproxy_port, zope_family_address_list)) -%} {% if has_webdav -%} {% set internal_scheme = 'http' -%}{# mod_rewrite does not recognise webdav scheme -#} @@ -89,7 +141,8 @@ ipv6 = {{ zope_address.split(']:')[0][1:] }} {% set internal_scheme = 'http' -%} {% set external_scheme = 'https' -%} {% endif -%} -{% do apache_dict.__setitem__(family_name, (next_port(), external_scheme, internal_scheme ~ '://' ~ ipv4 ~ ':' ~ haproxy_port ~ slapparameter_dict['backend-path'])) -%} +{% set ssl_authentication = slapparameter_dict['ssl-authentication-dict'].get(family_name, False) -%} +{% do apache_dict.__setitem__(family_name, (next_port(), external_scheme, internal_scheme ~ '://' ~ ipv4 ~ ':' ~ haproxy_port ~ backend_path, ssl_authentication)) -%} {% endfor -%} [haproxy-cfg-parameter-dict] @@ -122,6 +175,7 @@ crl = ${directory:apache-conf}/crl.pem backend-list = {{ dumps(apache_dict.values()) }} ip-list = {{ dumps(apache_ip_list) }} pid-file = ${directory:run}/apache.pid +log-dir = ${directory:log} error-log = ${directory:log}/apache-error.log access-log = ${directory:log}/apache-access.log # Apache 2.4's default value (60 seconds) can be a bit too short @@ -145,6 +199,18 @@ context = section parameter_dict apache-conf-parameter-dict recipe = slapos.cookbook:wrapper wrapper-path = ${directory:services}/apache command-line = "{{ parameter_dict['apache'] }}/bin/httpd" -f "${apache-conf:rendered}" -DFOREGROUND +wait-for-files = + ${apache-conf-ssl:cert} + ${apache-conf-ssl:key} + +[apache-graceful] +recipe = collective.recipe.template +input = inline: + #!/bin/sh + kill -USR1 "$(cat '${apache-conf-parameter-dict:pid-file}')" + +output = ${directory:bin}/apache-httpd-graceful +mode = 700 [{{ section('apache-promise') }}] # Check any apache port in ipv4, expect other ports and ipv6 to behave consistently @@ -155,7 +221,7 @@ port = {{ apache_dict.values()[0][0] }} [publish] recipe = slapos.cookbook:publish.serialised -{% for family_name, (apache_port, scheme, _) in apache_dict.items() -%} +{% for family_name, (apache_port, scheme, _, _) in apache_dict.items() -%} {{ family_name ~ '-v6' }} = {% if ipv6_set %}{{ scheme ~ '://[' ~ ipv6 ~ ']:' ~ apache_port }}{% endif %} {{ family_name }} = {{ scheme ~ '://' ~ ipv4 ~ ':' ~ apache_port }} {% endfor -%} @@ -167,6 +233,11 @@ key = ${apache-ssl-key:rendered} cert = ${apache-ssl-cert:rendered} {{ simplefile('apache-ssl-key', '${apache-conf-ssl:key}', ssl_parameter_dict['key']) }} {{ simplefile('apache-ssl-cert', '${apache-conf-ssl:cert}', ssl_parameter_dict['cert']) }} +{% elif caucase_url -%} +key = ${apache-conf-ssl:key} +cert = ${apache-conf-ssl:cert} + +{{ request_cert('erp5', 'instance.apache@erp5') }} {% else %} recipe = plone.recipe.command command = "{{ parameter_dict['openssl'] }}/bin/openssl" req -newkey rsa -batch -new -x509 -days 3650 -nodes -keyout "${:key}" -out "${:cert}" @@ -180,6 +251,10 @@ cert = ${apache-ssl-ca:rendered} crl = ${apache-ssl-crl:rendered} {{ simplefile('apache-ssl-ca', '${apache-conf-ssl:ca-cert}', ssl_parameter_dict['ca-cert']) }} {{ simplefile('apache-ssl-crl', '${apache-conf-ssl:crl}', ssl_parameter_dict['crl']) }} +{% elif caucase_url -%} +cert = ${apache-conf-ssl:ca-cert} +crl = ${apache-conf-ssl:crl} + {% else %} cert = crl = diff --git a/stack/erp5/instance-erp5.cfg.in b/stack/erp5/instance-erp5.cfg.in index ecdaa0abd..c5fe2e971 100644 --- a/stack/erp5/instance-erp5.cfg.in +++ b/stack/erp5/instance-erp5.cfg.in @@ -9,6 +9,8 @@ {% set has_jupyter = jupyter_dict.get('enable', jupyter_enable_default.lower() in ('true', 'yes')) -%} {% set jupyter_zope_family = jupyter_dict.get('zope-family', '') -%} {% set monitor_base_url_dict = {} -%} +{% set caucase_url = slapparameter_dict.get('caucase', {}).pop('url', '') -%} +{% set crl_update_period = slapparameter_dict.get('caucase', {}).pop('crl-update-periodicity', 'daily') -%} [request-common] <= request-common-base config-use-ipv6 = {{ dumps(slapparameter_dict.get('use-ipv6', False)) }} @@ -50,6 +52,14 @@ config-{{ k }} = {{ '${' ~ v ~ '}' }} connection-url = smtp://127.0.0.2:0/ {%- endif %} +{% if caucase_url -%} +{% do publish_dict.__setitem__('caucase-http-url', caucase_url) -%} +[request-caucase] +connection-http-url = {{ caucase_url }} +{%- else %} +{{ request('caucase', 'caucase', 'caucase', {'server-port': 8890, 'server-https-port': 8891}, {'http-url': True, 'https-url': False}) }} +{% endif -%} + {# ZODB -#} {% set zodb_dict = {} -%} {% set storage_dict = {} -%} @@ -137,6 +147,7 @@ return = {% endif -%} config-bt5 = {{ dumps(slapparameter_dict.get('bt5', bt5_default_list)) }} config-bt5-repository-url = {{ dumps(slapparameter_dict.get('bt5-repository-url', local_bt5_repository)) }} +config-caucase-url = ${request-caucase:connection-http-url} config-cloudooo-url = ${request-cloudooo:connection-url} config-deadlock-debugger-password = ${publish-early:deadlock-debugger-password} config-developer-list = {{ dumps(slapparameter_dict.get('developer-list', [inituser_login])) }} @@ -168,17 +179,22 @@ config-tidstorage-port = ${request-zodb:connection-tidstorage-port} software-type = zope {% set zope_family_dict = {} -%} +{% set zope_backend_path_dict = {} -%} +{% set ssl_authentication_dict = {} -%} {% set jupyter_zope_family_default = [] -%} {% for custom_name, zope_parameter_dict in zope_partition_dict.items() -%} {% set partition_name = 'zope-' ~ custom_name -%} {% set section_name = 'request-' ~ partition_name -%} {% set zope_family = zope_parameter_dict.get('family', 'default') -%} +{% set backend_path = zope_parameter_dict.get('backend-path', '/') % {'site-id': site_id} %} {# # default jupyter zope family is first zope family. -#} {# # use list.append() to update it, because in jinja2 set changes only local scope. -#} {% if not jupyter_zope_family_default -%} {% do jupyter_zope_family_default.append(zope_family) -%} {% endif -%} {% do zope_family_dict.setdefault(zope_family, []).append(section_name) -%} +{% do zope_backend_path_dict.__setitem__(zope_family, backend_path) -%} +{% do ssl_authentication_dict.__setitem__(zope_family, zope_parameter_dict.get('ssl-authentication', False)) -%} [{{ section_name }}] <= request-zope-base name = {{ partition_name }} @@ -256,10 +272,12 @@ config-{{ name }} = {{ ' ${' ~ zope_section_id ~ ':connection-zope-address-list} {% endfor -%} # XXX: should those really be same for all families ? config-haproxy-server-check-path = {{ dumps(balancer_dict.get('haproxy-server-check-path', '/') % {'site-id': site_id}) }} -config-backend-path = {{ dumps(balancer_dict.get('apache-backend-path', '/') % {'site-id': site_id}) }} config-ssl = {{ dumps(balancer_dict.get('ssl', {})) }} config-monitor-passwd = ${monitor-htpasswd:passwd} - +config-caucase-url = ${request-caucase:connection-http-url} +config-crl-update-periodicity = {{ crl_update_period }} +config-backend-path-dict = {{ dumps(zope_backend_path_dict) }} +config-ssl-authentication-dict = {{ dumps(ssl_authentication_dict) }} [request-frontend-base] {% if has_frontend -%} diff --git a/stack/erp5/instance.cfg.in b/stack/erp5/instance.cfg.in index 04de68fb1..18d678a20 100644 --- a/stack/erp5/instance.cfg.in +++ b/stack/erp5/instance.cfg.in @@ -1,5 +1,7 @@ [buildout] -extends = {{ instance_common_cfg }} +extends = + {{ instance_common_cfg }} + {{ caucase_template }} [jinja2-template-base] mode = 644 @@ -97,6 +99,7 @@ bin-directory = {{ bin_directory }} apachedex-location = {{ bin_directory }}/apachedex run-apachedex-location = {{ bin_directory }}/runApacheDex 6tunnel = {{ sixtunnel_location }} +curl-location = {{ curl_location }} dash = {{ dash_location }} template-haproxy-cfg = {{ template_haproxy_cfg }} template-apache-conf = {{ template_apache_conf }} @@ -216,6 +219,7 @@ create-erp5-site = dynamic-template-create-erp5-site:rendered RootSoftwareInstance = ${:default} # Internal software types kumofs = dynamic-template-kumofs:rendered +caucase = dynamic-template-caucase:rendered cloudooo = dynamic-template-cloudooo:rendered mariadb = dynamic-template-mariadb:rendered balancer = dynamic-template-balancer:rendered -- 2.30.9