diff --git a/data/Dockerfiles/acme/Dockerfile b/data/Dockerfiles/acme/Dockerfile index 944228ecd..a17064fef 100644 --- a/data/Dockerfiles/acme/Dockerfile +++ b/data/Dockerfiles/acme/Dockerfile @@ -1,10 +1,9 @@ -FROM alpine:3.8 +FROM alpine:3.9 LABEL maintainer "Andre Peters " RUN apk add --update --no-cache \ bash \ - acme-client \ curl \ openssl \ bind-tools \ @@ -12,7 +11,10 @@ RUN apk add --update --no-cache \ mariadb-client \ redis \ tini \ - tzdata + tzdata \ + py-pip \ + && pip install --upgrade pip \ + && pip install acme-tiny COPY docker-entrypoint.sh /srv/docker-entrypoint.sh COPY expand6.sh /srv/expand6.sh diff --git a/data/Dockerfiles/acme/docker-entrypoint.sh b/data/Dockerfiles/acme/docker-entrypoint.sh index 8373daea3..df1b837bd 100755 --- a/data/Dockerfiles/acme/docker-entrypoint.sh +++ b/data/Dockerfiles/acme/docker-entrypoint.sh @@ -17,7 +17,7 @@ log_f() { redis-cli -h redis LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"base64,$(printf '%s' "${1}")\"}" > /dev/null else redis-cli -h redis LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${1}" | \ - tr '%&;$"_[]{}-\r\n' ' ')\"}" > /dev/null + tr '%&;$"[]{}-\r\n' ' ')\"}" > /dev/null fi } @@ -36,7 +36,12 @@ log_f "OK" no_date ACME_BASE=/var/lib/acme SSL_EXAMPLE=/var/lib/ssl-example -mkdir -p ${ACME_BASE}/acme/private +mkdir -p ${ACME_BASE}/acme + +# Migrate +[[ -f ${ACME_BASE}/acme/private/privkey.pem ]] && mv ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/acme/key.pem +[[ -f ${ACME_BASE}/acme/private/account.key ]] && mv ${ACME_BASE}/acme/private/account.key ${ACME_BASE}/acme/account.pem + reload_configurations(){ # Reading container IDs @@ -112,6 +117,19 @@ get_ipv6(){ echo ${IPV6} } +verify_challenge_path(){ + # verify_challenge_path URL 4|6 + RAND_FILE=${RANDOM}${RANDOM}${RANDOM} + touch /var/www/acme/${RAND_FILE} + if [[ "$(curl -${2} http://${1}/.well-known/acme-challenge/${RAND_FILE} --write-out %{http_code} --silent --output /dev/null)" == "200" ]]; then + rm /var/www/acme/${RAND_FILE} + return 0 + else + rm /var/www/acme/${RAND_FILE} + return 1 + fi +} + [[ ! -f ${ACME_BASE}/dhparams.pem ]] && cp ${SSL_EXAMPLE}/dhparams.pem ${ACME_BASE}/dhparams.pem if [[ -f ${ACME_BASE}/cert.pem ]] && [[ -f ${ACME_BASE}/key.pem ]]; then @@ -120,20 +138,13 @@ if [[ -f ${ACME_BASE}/cert.pem ]] && [[ -f ${ACME_BASE}/key.pem ]]; then log_f "Found certificate with issuer other than mailcow snake-oil CA and Let's Encrypt, skipping ACME client..." sleep 3650d exec $(readlink -f "$0") - else - declare -a SAN_ARRAY_NOW - SAN_NAMES=$(openssl x509 -noout -text -in ${ACME_BASE}/cert.pem | awk '/X509v3 Subject Alternative Name/ {getline;gsub(/ /, "", $0); print}' | tr -d "DNS:") - if [[ ! -z ${SAN_NAMES} ]]; then - IFS=',' read -a SAN_ARRAY_NOW <<< ${SAN_NAMES} - log_f "Found Let's Encrypt or mailcow snake-oil CA issued certificate with SANs: ${SAN_ARRAY_NOW[*]}" - fi fi else - if [[ -f ${ACME_BASE}/acme/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/privkey.pem ]]; then - if verify_hash_match ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/acme/private/privkey.pem; then + if [[ -f ${ACME_BASE}/acme/cert.pem ]] && [[ -f ${ACME_BASE}/acme/key.pem ]]; then + if verify_hash_match ${ACME_BASE}/acme/cert.pem ${ACME_BASE}/acme/key.pem; then log_f "Restoring previous acme certificate and restarting script..." - cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem - cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem + cp ${ACME_BASE}/acme/cert.pem ${ACME_BASE}/cert.pem + cp ${ACME_BASE}/acme/key.pem ${ACME_BASE}/key.pem # Restarting with env var set to trigger a restart, exec env TRIGGER_RESTART=1 $(readlink -f "$0") fi @@ -150,24 +161,59 @@ log_f "Waiting for database... " while ! mysqladmin status --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do sleep 2 done + +# Waiting for domain table +log_f "Waiting for domain table... " no_nl +while [[ -z ${DOMAIN_TABLE} ]]; do + curl --silent http://nginx/ >/dev/null 2>&1 + DOMAIN_TABLE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'domain'" -Bs) + [[ -z ${DOMAIN_TABLE} ]] && sleep 10 +done +log_f "OK" no_date + log_f "Initializing, please wait... " - while true; do + + # Re-using previous acme-mailcow account and domain keys + if [[ ! -f ${ACME_BASE}/acme/key.pem ]]; then + log_f "Generating missing domain private key..." + openssl genrsa 4096 > ${ACME_BASE}/acme/key.pem + else + log_f "Using existing domain key ${ACME_BASE}/acme/key.pem" + fi + if [[ ! -f ${ACME_BASE}/acme/account.pem ]]; then + log_f "Generating missing Lets Encrypt account key..." + openssl genrsa 4096 > ${ACME_BASE}/acme/account.pem + else + log_f "Using existing Lets Encrypt account key ${ACME_BASE}/acme/account.pem" + fi + + # Skipping IP check when we like to live dangerously if [[ "${SKIP_IP_CHECK}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then SKIP_IP_CHECK=y fi + + # Cleaning up and init validation arrays unset SQL_DOMAIN_ARR unset VALIDATED_CONFIG_DOMAINS unset ADDITIONAL_VALIDATED_SAN + unset ADDITIONAL_WC_ARR + unset ADDITIONAL_SAN_ARR + unset SAN_CHANGE + unset SAN_ARRAY_NOW + unset ORPHANED_SAN + unset ADDED_SAN + SAN_CHANGE=0 + declare -a SAN_ARRAY_NOW + declare -a ORPHANED_SAN + declare -a ADDED_SAN declare -a SQL_DOMAIN_ARR declare -a VALIDATED_CONFIG_DOMAINS declare -a ADDITIONAL_VALIDATED_SAN + declare -a ADDITIONAL_WC_ARR + declare -a ADDITIONAL_SAN_ARR IFS=',' read -r -a TMP_ARR <<< "${ADDITIONAL_SAN}" - log_f "Detecting IP addresses... " no_nl - - unset ADDITIONAL_WC_ARR - unset ADDITIONAL_SAN_ARR for i in "${TMP_ARR[@]}" ; do if [[ "$i" =~ \.\*$ ]]; then ADDITIONAL_WC_ARR+=(${i::-2}) @@ -177,6 +223,8 @@ while true; do done ADDITIONAL_WC_ARR+=('autodiscover') + # Start IP detection + log_f "Detecting IP addresses... " no_nl IPV4=$(get_ipv4) IPV6=$(get_ipv6) log_f "OK" no_date @@ -194,23 +242,15 @@ while true; do fi fi - log_f "Waiting for domain table... " no_nl - while [[ -z ${DOMAIN_TABLE} ]]; do - curl --silent http://nginx/ >/dev/null 2>&1 - DOMAIN_TABLE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'domain'" -Bs) - [[ -z ${DOMAIN_TABLE} ]] && sleep 10 - done - log_f "OK" no_date - + ######################################### + # IP and webroot challenge verification # while read domains; do SQL_DOMAIN_ARR+=("${domains}") done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain WHERE backupmx=0" -Bs) for SQL_DOMAIN in "${SQL_DOMAIN_ARR[@]}"; do for SUBDOMAIN in "${ADDITIONAL_WC_ARR[@]}"; do - if [[ "${SUBDOMAIN}.${SQL_DOMAIN}" == ${MAILCOW_HOSTNAME} ]]; then - log_f "Skipping mailcow hostname (${MAILCOW_HOSTNAME}), will be added anyway" - else + if [[ "${SUBDOMAIN}.${SQL_DOMAIN}" != "${MAILCOW_HOSTNAME}" ]]; then A_SUBDOMAIN=$(dig A ${SUBDOMAIN}.${SQL_DOMAIN} +short | tail -n 1) AAAA_SUBDOMAIN=$(dig AAAA ${SUBDOMAIN}.${SQL_DOMAIN} +short | tail -n 1) # Check if CNAME without v6 enabled target @@ -220,16 +260,24 @@ while true; do if [[ ! -z ${AAAA_SUBDOMAIN} ]]; then log_f "Found AAAA record for ${SUBDOMAIN}.${SQL_DOMAIN}: ${AAAA_SUBDOMAIN} - skipping A record check" if [[ $(expand ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}) == $(expand ${AAAA_SUBDOMAIN}) ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then - log_f "Confirmed AAAA record ${SUBDOMAIN}.${SQL_DOMAIN}" - VALIDATED_CONFIG_DOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}") + if verify_challenge_path "${SUBDOMAIN}.${SQL_DOMAIN}" 6; then + log_f "Confirmed AAAA record ${AAAA_SUBDOMAIN}" + VALIDATED_CONFIG_DOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}") + else + log_f "Confirmed AAAA record ${AAAA_SUBDOMAIN}, but HTTP validation failed" + fi else log_f "Cannot match your IP ${IPV6:-NO_IPV6_LINK} against hostname ${SUBDOMAIN}.${SQL_DOMAIN} ($(expand ${AAAA_SUBDOMAIN}))" fi elif [[ ! -z ${A_SUBDOMAIN} ]]; then log_f "Found A record for ${SUBDOMAIN}.${SQL_DOMAIN}: ${A_SUBDOMAIN}" if [[ ${IPV4:-ERR} == ${A_SUBDOMAIN} ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then - log_f "Confirmed A record ${SUBDOMAIN}.${SQL_DOMAIN}" - VALIDATED_CONFIG_DOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}") + if verify_challenge_path "${SUBDOMAIN}.${SQL_DOMAIN}" 4; then + log_f "Confirmed A record ${A_SUBDOMAIN}" + VALIDATED_CONFIG_DOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}") + else + log_f "Confirmed AAAA record ${A_SUBDOMAIN}, but HTTP validation failed" + fi else log_f "Cannot match your IP ${IPV4} against hostname ${SUBDOMAIN}.${SQL_DOMAIN} (${A_SUBDOMAIN})" fi @@ -249,16 +297,24 @@ while true; do if [[ ! -z ${AAAA_MAILCOW_HOSTNAME} ]]; then log_f "Found AAAA record for ${MAILCOW_HOSTNAME}: ${AAAA_MAILCOW_HOSTNAME} - skipping A record check" if [[ $(expand ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}) == $(expand ${AAAA_MAILCOW_HOSTNAME}) ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then - log_f "Confirmed AAAA record ${MAILCOW_HOSTNAME}" - VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} + if verify_challenge_path "${MAILCOW_HOSTNAME}" 6; then + log_f "Confirmed AAAA record ${AAAA_MAILCOW_HOSTNAME}" + VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} + else + log_f "Confirmed AAAA record ${A_MAILCOW_HOSTNAME}, but HTTP validation failed" + fi else log_f "Cannot match your IP ${IPV6:-NO_IPV6_LINK} against hostname ${MAILCOW_HOSTNAME} ($(expand ${AAAA_MAILCOW_HOSTNAME}))" fi elif [[ ! -z ${A_MAILCOW_HOSTNAME} ]]; then log_f "Found A record for ${MAILCOW_HOSTNAME}: ${A_MAILCOW_HOSTNAME}" if [[ ${IPV4:-ERR} == ${A_MAILCOW_HOSTNAME} ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then - log_f "Confirmed A record ${A_MAILCOW_HOSTNAME}" - VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} + if verify_challenge_path "${MAILCOW_HOSTNAME}" 4; then + log_f "Confirmed A record ${A_MAILCOW_HOSTNAME}" + VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} + else + log_f "Confirmed A record ${A_MAILCOW_HOSTNAME}, but HTTP validation failed" + fi else log_f "Cannot match your IP ${IPV4} against hostname ${MAILCOW_HOSTNAME} (${A_MAILCOW_HOSTNAME})" fi @@ -290,16 +346,24 @@ while true; do if [[ ! -z ${AAAA_SAN} ]]; then log_f "Found AAAA record for ${SAN}: ${AAAA_SAN} - skipping A record check" if [[ $(expand ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}) == $(expand ${AAAA_SAN}) ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then - log_f "Confirmed AAAA record ${SAN}" - ADDITIONAL_VALIDATED_SAN+=("${SAN}") + if verify_challenge_path "${SAN}" 6; then + log_f "Confirmed AAAA record ${AAAA_SAN}" + ADDITIONAL_VALIDATED_SAN+=("${SAN}") + else + log_f "Confirmed AAAA record ${AAAA_SAN}, but HTTP validation failed" + fi else log_f "Cannot match your IP ${IPV6:-NO_IPV6_LINK} against hostname ${SAN} ($(expand ${AAAA_SAN}))" fi elif [[ ! -z ${A_SAN} ]]; then log_f "Found A record for ${SAN}: ${A_SAN}" if [[ ${IPV4:-ERR} == ${A_SAN} ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then - log_f "Confirmed A record ${A_SAN}" - ADDITIONAL_VALIDATED_SAN+=("${SAN}") + if verify_challenge_path "${SAN}" 4; then + log_f "Confirmed A record ${A_SAN}" + ADDITIONAL_VALIDATED_SAN+=("${SAN}") + else + log_f "Confirmed A record ${A_SAN}, but HTTP validation failed" + fi else log_f "Cannot match your IP ${IPV4} against hostname ${SAN} (${A_SAN})" fi @@ -317,123 +381,97 @@ while true; do exec $(readlink -f "$0") fi - array_diff ORPHANED_SAN SAN_ARRAY_NOW ALL_VALIDATED - if [[ ! -z ${ORPHANED_SAN[*]} ]] && [[ ${ISSUER} != *"mailcow"* ]]; then - DATE=$(date +%Y-%m-%d_%H_%M_%S) - log_f "Found orphaned SAN ${ORPHANED_SAN[*]} in certificate, moving old files to ${ACME_BASE}/acme/private/${DATE}.bak/, keeping key file..." - mkdir -p ${ACME_BASE}/acme/private/${DATE}.bak/ - [[ -f ${ACME_BASE}/acme/private/account.key ]] && mv ${ACME_BASE}/acme/private/account.key ${ACME_BASE}/acme/private/${DATE}.bak/ - [[ -f ${ACME_BASE}/acme/fullchain.pem ]] && mv ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/acme/private/${DATE}.bak/ - [[ -f ${ACME_BASE}/acme/cert.pem ]] && mv ${ACME_BASE}/acme/cert.pem ${ACME_BASE}/acme/private/${DATE}.bak/ - cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/acme/private/${DATE}.bak/ # Keep key for TLSA 3 1 1 records + # Collecting SANs from active certificate + SAN_NAMES=$(openssl x509 -noout -text -in ${ACME_BASE}/cert.pem | awk '/X509v3 Subject Alternative Name/ {getline;gsub(/ /, "", $0); print}' | tr -d "DNS:") + if [[ ! -z ${SAN_NAMES} ]]; then + IFS=',' read -a SAN_ARRAY_NOW <<< ${SAN_NAMES} fi + # Finding difference in SAN array now vs. SAN array by current configuration + array_diff ORPHANED_SAN SAN_ARRAY_NOW ALL_VALIDATED + if [[ ! -z ${ORPHANED_SAN[*]} ]]; then + log_f "Found orphaned SANs ${ORPHANED_SAN[*]}" + SAN_CHANGE=1 + fi + array_diff ADDED_SAN ALL_VALIDATED SAN_ARRAY_NOW + if [[ ! -z ${ADDED_SAN[*]} ]]; then + log_f "Found new SANs ${ADDED_SAN[*]}" + SAN_CHANGE=1 + fi + + if [[ ${SAN_CHANGE} == 0 ]]; then + # Certificate did not change but could be due for renewal (4 weeks) + if ! openssl x509 -checkend 1209600 -noout -in ${ACME_BASE}/cert.pem; then + log_f "Certificate is due for renewal (< 2 weeks)" + else + log_f "Certificate validation done, neither changed nor due for renewal, sleeping for another day." + sleep 1d + continue + fi + fi + + DATE=$(date +%Y-%m-%d_%H_%M_%S) + log_f "Creating backups in ${ACME_BASE}/backups/${DATE}/ ..." + mkdir -p ${ACME_BASE}/backups/${DATE}/ + [[ -f ${ACME_BASE}/acme/acme.csr ]] && cp ${ACME_BASE}/acme/acme.csr ${ACME_BASE}/backups/${DATE}/ + [[ -f ${ACME_BASE}/acme/cert.pem ]] && cp ${ACME_BASE}/acme/cert.pem ${ACME_BASE}/backups/${DATE}/ + [[ -f ${ACME_BASE}/acme/key.pem ]] && cp ${ACME_BASE}/acme/key.pem ${ACME_BASE}/backups/${DATE}/ + [[ -f ${ACME_BASE}/acme/account.pem ]] && cp ${ACME_BASE}/acme/account.pem ${ACME_BASE}/backups/${DATE}/ + + # Generating CSR + printf "[SAN]\nsubjectAltName=" > /tmp/_SAN + printf "DNS:%s," "${ALL_VALIDATED[@]}" >> /tmp/_SAN + sed -i '$s/,$//' /tmp/_SAN + openssl req -new -sha256 -key ${ACME_BASE}/acme/key.pem -subj "/" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf /tmp/_SAN) > ${ACME_BASE}/acme/acme.csr + if [[ "${LE_STAGING}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then log_f "Using Let's Encrypt staging servers" - STAGING_PARAMETER="-s" + STAGING_PARAMETER='--directory-url https://acme-staging-v02.api.letsencrypt.org/directory' else STAGING_PARAMETER= fi - ACME_RESPONSE=$(acme-client \ - -v -e -b -N -n ${STAGING_PARAMETER} \ - -a 'https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf' \ - -f ${ACME_BASE}/acme/private/account.key \ - -k ${ACME_BASE}/acme/private/privkey.pem \ - -c ${ACME_BASE}/acme \ - ${ALL_VALIDATED[*]} 2>&1 | tee /dev/fd/5) - case "$?" in - 0) # new certs - ACME_RESPONSE_B64=$(echo ${ACME_RESPONSE} | openssl enc -e -A -base64) - log_f "${ACME_RESPONSE_B64}" redis_only b64 - # cp the new certificates and keys - cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem - cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem + # acme-tiny writes info to stderr and ceritifcate to stdout + # The redirects will do the following: + # - redirect stdout to temp certificate file + # - redirect acme-tiny stderr to stdout (logs to variable ACME_RESPONSE) + # - tee stderr to get live output and log to dockerd - # restart docker containers - if ! verify_hash_match ${ACME_BASE}/cert.pem ${ACME_BASE}/key.pem; then - log_f "Certificate was successfully requested, but key and certificate have non-matching hashes, restoring mailcow snake-oil and restarting containers..." - cp ${SSL_EXAMPLE}/cert.pem ${ACME_BASE}/cert.pem - cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem - fi - reload_configurations - ;; - 1) # failure - ACME_RESPONSE_B64=$(echo ${ACME_RESPONSE} | openssl enc -e -A -base64) + ACME_RESPONSE=$(acme-tiny ${STAGING_PARAMETER} \ + --account-key ${ACME_BASE}/acme/account.pem \ + --disable-check \ + --csr ${ACME_BASE}/acme/acme.csr \ + --acme-dir /var/www/acme/ 2>&1 > /tmp/_cert.pem | tee /dev/fd/5) + + case "$?" in + 0) # cert requested + ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64) log_f "${ACME_RESPONSE_B64}" redis_only b64 - if [[ $ACME_RESPONSE =~ "No registration exists" ]]; then - log_f "Registration keys are invalid, deleting old keys and restarting..." - rm ${ACME_BASE}/acme/private/account.key + log_f "Deploying..." + # Deploy the new certificate and key + # Moving temp cert to acme/cert.pem + if verify_hash_match /tmp/_cert.pem ${ACME_BASE}/acme/key.pem; then + mv /tmp/_cert.pem ${ACME_BASE}/acme/cert.pem + cp ${ACME_BASE}/acme/cert.pem ${ACME_BASE}/cert.pem + cp ${ACME_BASE}/acme/key.pem ${ACME_BASE}/key.pem + reload_configurations + rm /var/www/acme/* + log_f "Certificate successfully deployed, removing backup, sleeping 1d" + sleep 1d + else + log_f "Certificate was successfully requested, but key and certificate have non-matching hashes, ignoring certificate" + log_f "Retrying in 30 minutes..." + sleep 30m exec $(readlink -f "$0") fi - if [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ]]; then - log_f "Error requesting certificate, restoring previous certificate from backup and restarting containers...." - cp ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ${ACME_BASE}/cert.pem - cp ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ${ACME_BASE}/key.pem - TRIGGER_RESTART=1 - elif [[ -f ${ACME_BASE}/acme/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/privkey.pem ]]; then - log_f "Error requesting certificate, restoring from previous acme request and restarting containers..." - cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem - cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem - TRIGGER_RESTART=1 - fi - if ! verify_hash_match ${ACME_BASE}/cert.pem ${ACME_BASE}/key.pem; then - log_f "Error verifying certificates, restoring mailcow snake-oil and restarting containers..." - cp ${SSL_EXAMPLE}/cert.pem ${ACME_BASE}/cert.pem - cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem - TRIGGER_RESTART=1 - fi - [[ ${TRIGGER_RESTART} == 1 ]] && reload_configurations - log_f "Retrying in 30 minutes..." - sleep 30m - exec $(readlink -f "$0") ;; - 2) # no change - ACME_RESPONSE_B64=$(echo ${ACME_RESPONSE} | openssl enc -e -A -base64) + *) # non-zero is non-fun + ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64) log_f "${ACME_RESPONSE_B64}" redis_only b64 - if ! diff ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem; then - log_f "Certificate was not changed, but active certificate does not match the verified certificate, fixing and restarting containers..." - cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem - cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem - TRIGGER_RESTART=1 - fi - if ! verify_hash_match ${ACME_BASE}/cert.pem ${ACME_BASE}/key.pem; then - log_f "Certificate was not changed, but hashes do not match, restoring from previous acme request and restarting containers..." - cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem - cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem - TRIGGER_RESTART=1 - fi - log_f "Certificate was not changed" - [[ ${TRIGGER_RESTART} == 1 ]] && reload_configurations - ;; - *) # unspecified - ACME_RESPONSE_B64=$(echo ${ACME_RESPONSE} | openssl enc -e -A -base64) - log_f "${ACME_RESPONSE_B64}" redis_only b64 - if [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ]]; then - log_f "Error requesting certificate, restoring previous certificate from backup and restarting containers...." - cp ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ${ACME_BASE}/cert.pem - cp ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ${ACME_BASE}/key.pem - TRIGGER_RESTART=1 - elif [[ -f ${ACME_BASE}/acme/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/privkey.pem ]]; then - log_f "Error requesting certificate, restoring from previous acme request and restarting containers..." - cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem - cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem - TRIGGER_RESTART=1 - fi - if ! verify_hash_match ${ACME_BASE}/cert.pem ${ACME_BASE}/key.pem; then - log_f "Error verifying certificates, restoring mailcow snake-oil..." - cp ${SSL_EXAMPLE}/cert.pem ${ACME_BASE}/cert.pem - cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem - TRIGGER_RESTART=1 - fi - [[ ${TRIGGER_RESTART} == 1 ]] && reload_configurations log_f "Retrying in 30 minutes..." sleep 30m exec $(readlink -f "$0") ;; esac - log_f "ACME certificate validation done. Sleeping for another day." - sleep 1d - done diff --git a/data/web/lang/lang.nl.php b/data/web/lang/lang.nl.php index 22351121c..55813276c 100644 --- a/data/web/lang/lang.nl.php +++ b/data/web/lang/lang.nl.php @@ -46,7 +46,7 @@ $lang['danger']['malformed_username'] = "Ongeldige gebruikersnaam"; $lang['info']['awaiting_tfa_confirmation'] = "In afwachting van tweefactorauthenticatie..."; $lang['success']['logged_in_as'] = "Succesvol ingelogd als %s"; $lang['danger']['login_failed'] = "Aanmelding mislukt"; -$lang['danger']['set_acl_failed'] = "ALC kon niet worden ingesteld"; +$lang['danger']['set_acl_failed'] = "Toegangscontrole kon niet worden ingesteld"; $lang['danger']['no_user_defined'] = "Geen gebruiker gespecificeerd"; $lang['danger']['script_empty'] = "Script dient ingevuld te worden"; $lang['danger']['sieve_error'] = "Sieve-fout: %s"; @@ -56,7 +56,7 @@ $lang['danger']['domain_cannot_match_hostname'] = "Het domein dient af te wijken $lang['warning']['domain_added_sogo_failed'] = "Domein is toegevoegd, maar het hestarten van SOGo mislukte. Controleer de serverlogs."; $lang['danger']['rl_timeframe'] = "Ratelimit-tijdsbestek is ongeldig"; $lang['success']['rl_saved'] = "Ratelimit voor object %s is opgeslagen"; -$lang['success']['acl_saved'] = "ACL voor object %s is opgeslagen"; +$lang['success']['acl_saved'] = "Toegangscontrole voor object %s is opgeslagen"; $lang['success']['deleted_syncjobs'] = "Synchronisatietaken %s zijn verwijderd"; $lang['success']['deleted_syncjob'] = "Synchronisatietaak %s is verwijderd"; $lang['success']['delete_filters'] = "Filters %s zijn verwijderd"; @@ -154,7 +154,7 @@ $lang['danger']['max_quota_in_use'] = "Postvakquotum moet gelijk zijn aan, of gr $lang['danger']['domain_quota_m_in_use'] = "Domeinquotum moet gelijk zijn aan, of groter zijn dan %s MiB"; $lang['danger']['mailboxes_in_use'] = "Maximaal aantal postvakken moet gelijk zijn aan, of groter zijn dan %d"; $lang['danger']['aliases_in_use'] = "Maximaal aantal aliassen moet gelijk zijn aan, of groter zijn dan %d"; -$lang['danger']['sender_acl_invalid'] = "ACL-waarde van afzender %s is ongeldig"; +$lang['danger']['sender_acl_invalid'] = "Toegangscontrole van afzender %s is ongeldig"; $lang['danger']['domain_not_empty'] = "Kan geen domein in gebruik verwijderen"; $lang['danger']['validity_missing'] = 'Wijs een geldigheidstermijn toe'; $lang['user']['loading'] = "Bezig met laden..."; @@ -229,12 +229,12 @@ $lang['user']['tag_handling'] = 'Omgaan met e-mailtags'; $lang['user']['tag_in_subfolder'] = 'In submap'; $lang['user']['tag_in_subject'] = 'In onderwerp'; $lang['user']['tag_in_none'] = 'Niets doen'; -$lang['user']['tag_help_explain'] = 'In submap: er wordt een nieuwe map aangemaakt, genoemd naar de tag (bijv.: "INBOX/Apple").
In onderwerp: de tag wordt vóór het oorspronkelijke onderwerp geplaatst (bijv.: "[Apple] Uw bestelling").'; -$lang['user']['tag_help_example'] = 'Voorbeeld van een e-maildres met tag: ik+Apple@example.org'; +$lang['user']['tag_help_explain'] = 'In submap: er wordt een nieuwe map aangemaakt, genoemd naar de tag (bijv.: "INBOX/Tesla").
In onderwerp: de tag wordt vóór het oorspronkelijke onderwerp geplaatst (bijv.: "[Tesla] Uw serviceafspraak").'; +$lang['user']['tag_help_example'] = 'Voorbeeld van een e-maildres met tag: ik+Tesla@example.org'; $lang['user']['eas_reset'] = 'Herstel ActiveSync-apparaatcache'; $lang['user']['eas_reset_now'] = 'Herstel nu'; -$lang['user']['eas_reset_help'] = 'In de meeste gevallen verhelpt dit problemen met ActiveSync op uw apparaten
Let wel: alle onderdelen zullen opnieuw gedownload moeten worden!'; +$lang['user']['eas_reset_help'] = 'In de meeste gevallen verhelpt dit problemen met ActiveSync op je apparaten
Let wel: alle onderdelen zullen opnieuw gedownload moeten worden!'; $lang['user']['sogo_profile_reset'] = 'Herstel SOGo-profiel'; $lang['user']['sogo_profile_reset_now'] = 'Herstel nu'; @@ -251,9 +251,9 @@ $lang['user']['edit'] = 'Wijzig'; $lang['user']['remove'] = 'Verwijder'; $lang['user']['create_syncjob'] = 'Voeg een nieuwe synchronisatietaak toe'; -$lang['start']['mailcow_apps_detail'] = 'Gebruik een Mailcow-app om uw e-mails, agenda, contacten en meer te bekijken.'; +$lang['start']['mailcow_apps_detail'] = 'Gebruik een Mailcow-app om je e-mails, agenda, contacten en meer te bekijken.'; $lang['start']['mailcow_panel_detail'] = 'Domeinbeheerders kunnen postvakken en aliassen aanmaken, wijzigen en verwijderen. Ook kunnen ze domeinen aanpassen en informatie over deze verkrijgen.
Gebruikers kunnen tijdelijke aliassen aanmaken, hun wachtwoord aanpassen en de spamfilterinstellingen wijzigen.'; -$lang['start']['imap_smtp_server_auth_info'] = 'Gebruik uw volledige e-mailadres en het onversleutelde verificatiemechanisme.
De aanmeldgegevens worden versleuteld verstuurd.'; +$lang['start']['imap_smtp_server_auth_info'] = 'Gebruik je volledige e-mailadres en het bijbehorende (onversleutelde) verificatiemechanisme.
De aanmeldgegevens worden versleuteld verstuurd.'; $lang['start']['help'] = 'Toon/verberg hulppaneel'; $lang['header']['mailcow_settings'] = 'Instellingen'; $lang['header']['administration'] = 'Configuratie & details'; @@ -384,7 +384,14 @@ $lang['edit']['multiple_bookings'] = 'Meerdere boekingen'; $lang['edit']['kind'] = 'Soort'; $lang['edit']['resource'] = 'Hulpbron'; $lang['edit']['relayhost'] = 'Afzender-afhankelijke transportkaarten'; - +$lang['edit']['public_comment'] = 'Publiekelijke opmerking'; +$lang['mailbox']['public_comment'] = 'Publiekelijke opmerking'; +$lang['edit']['private_comment'] = 'Persoonlijke opmerking'; +$lang['mailbox']['private_comment'] = 'Persoonlijke opmerking'; +$lang['edit']['comment_info'] = 'Een persoonlijke opmerking is niet zichtbaar voor de gebruiker, terwijl een publiekelijke opmerking wel weergegeven zal worden in het overzicht van een gebruiker.'; +$lang['add']['public_comment'] = 'Publiekelijke opmerking'; +$lang['add']['private_comment'] = 'Persoonlijke opmerking'; +$lang['add']['comment_info'] = 'Een persoonlijke opmerking is niet zichtbaar voor de gebruiker, terwijl een publiekelijke opmerking wel weergegeven zal worden in het overzicht van een gebruiker.'; $lang['acl']['spam_alias'] = 'Tijdelijke aliassen'; $lang['acl']['tls_policy'] = 'Versleutelingsbeleid'; $lang['acl']['spam_score'] = 'Spamscore'; @@ -393,14 +400,28 @@ $lang['acl']['delimiter_action'] = 'Delimiter-actie'; $lang['acl']['syncjobs'] = 'Synchronisatietaken'; $lang['acl']['eas_reset'] = 'Herstel ActiveSync-apparaatcache'; $lang['acl']['sogo_profile_reset'] = 'Herstel SOGo-profiel'; -$lang['acl']['quarantine'] = 'Quarantaine'; +$lang['acl']['quarantine'] = 'Quarantaine-acties'; +$lang['acl']['quarantine_notification'] = 'Quarantaine-meldingen'; +$lang['acl']['quarantine_attachments'] = 'Quarantaine-bijlagen'; $lang['acl']['alias_domains'] = 'Voeg aliasdomeinen toe'; $lang['acl']['login_as'] = 'Log in als postvakgebruiker'; $lang['acl']['bcc_maps'] = 'BCC-kaarten'; $lang['acl']['filters'] = 'Filters'; $lang['acl']['ratelimit'] = 'Ratelimit'; $lang['acl']['recipient_maps'] = 'Ontvanger-kaarten'; -$lang['acl']['prohibited'] = 'Geweigerd door ACL'; +$lang['acl']['prohibited'] = 'Toegang geweigerd'; + +$lang['mailbox']['quarantine_notification'] = 'Quarantaine-meldingen'; +$lang['mailbox']['never'] = 'Nooit'; +$lang['mailbox']['hourly'] = 'Ieder uur'; +$lang['mailbox']['daily'] = 'Dagelijks'; +$lang['mailbox']['weekly'] = 'Wekelijks'; +$lang['user']['quarantine_notification'] = 'Quarantaine-meldingen'; +$lang['user']['never'] = 'Nooit'; +$lang['user']['hourly'] = 'Ieder uur'; +$lang['user']['daily'] = 'Dagelijks'; +$lang['user']['weekly'] = 'Wekelijks'; +$lang['user']['quarantine_notification_info'] = 'Zodra een melding is verzonden, worden de items als gelezen gemarkeerd en zullen er geen meldingen meer over diezelfde items verstuurd worden.'; $lang['add']['generate'] = 'genereer'; $lang['add']['syncjob'] = 'Voeg een nieuwe synchronisatietaak toe'; @@ -494,11 +515,11 @@ $lang['tfa']['disable_tfa'] = "Zet TFA uit tot de eerstvolgende succesvolle logi $lang['tfa']['confirm'] = "Bevestig"; $lang['tfa']['totp'] = "TOTP (Google Authenticator etc.)"; $lang['tfa']['select'] = "Selecteer..."; -$lang['tfa']['waiting_usb_auth'] = "In afwachting van USB-apparaat...

Druk nu op de knop van uw U2F-apparaat."; -$lang['tfa']['waiting_usb_register'] = "In afwachting van USB-apparaat...

Voer uw wachtwoord hierboven in en bevestig de registratie van het U2F-apparaat door op de knop van het apparaat te drukken."; -$lang['tfa']['scan_qr_code'] = "Scan de volgende QR-code met uw authenticatie-app:"; -$lang['tfa']['enter_qr_code'] = "Voer deze code in als uw apparaat geen QR-codes kan scannen:"; -$lang['tfa']['confirm_totp_token'] = "Bevestig de wijzigingen door de, door uw authenticatie-app gegenereerde code, in te voeren."; +$lang['tfa']['waiting_usb_auth'] = "In afwachting van USB-apparaat...

Druk nu op de knop van je U2F-apparaat."; +$lang['tfa']['waiting_usb_register'] = "In afwachting van USB-apparaat...

Voer je wachtwoord hierboven in en bevestig de registratie van het U2F-apparaat door op de knop van het apparaat te drukken."; +$lang['tfa']['scan_qr_code'] = "Scan de volgende QR-code met je authenticatie-app:"; +$lang['tfa']['enter_qr_code'] = "Voer deze code in als je apparaat geen QR-codes kan scannen:"; +$lang['tfa']['confirm_totp_token'] = "Bevestig de wijzigingen door de, door je authenticatie-app gegenereerde code, in te voeren."; $lang['admin']['rspamd-com_settings'] = 'Rspamd documentatie - Een beschrijving voor deze instelling zal automatisch worden gegenereerd, bekijk de onderstaande presets voor meer info.'; @@ -636,13 +657,15 @@ $lang['admin']['queue_unban'] = "markeer om toe te staan"; $lang['admin']['no_active_bans'] = "Geen actieve verbanningen"; $lang['admin']['quarantine'] = "Quarantaine"; -$lang['admin']['quarantine_retention_size'] = "Maximale retenties per postvak
Gebruik 0 om deze functionaliteit uit te zetten."; -$lang['admin']['quarantine_max_size'] = "Maximale grootte in MiB (mail die de limiet overschrijdt zal worden verwijderd)
0 betekent niet onbeperkt!"; -$lang['admin']['quarantine_exclude_domains'] = "Sluit domeinen en aliasdomeinen uit"; -$lang['admin']['quarantine_release_format'] = "Vrijgegeven items worden verstuurd als"; +$lang['admin']['quarantine_retention_size'] = "Maximale retenties per postvak:
Gebruik 0 om deze functionaliteit uit te zetten."; +$lang['admin']['quarantine_max_size'] = "Maximale grootte in MiB (mail die de limiet overschrijdt zal worden verwijderd):
0 betekent niet onbeperkt!"; +$lang['admin']['quarantine_exclude_domains'] = "Sluit de volgende domeinen en aliasdomeinen uit"; +$lang['admin']['quarantine_release_format'] = "Verstuur vrijgegeven items als"; $lang['admin']['quarantine_release_format_raw'] = "Origineel"; $lang['admin']['quarantine_release_format_att'] = "Bijlage"; - +$lang['admin']['quarantine_notification_sender'] = "Afzender van meldingen"; +$lang['admin']['quarantine_notification_subject'] = "Onderwerp van meldingen"; +$lang['admin']['quarantine_notification_html'] = "Meldingsjabloon:
Laat leeg om de standaardsjabloon te herstellen."; $lang['admin']['ui_texts'] = "UI-labels en teksten"; $lang['admin']['help_text'] = "Pas hulpteksten onder inlogvenster aan (HTML toegestaan)"; $lang['admin']['title_name'] = '"Mailcow UI" website-titel'; @@ -669,6 +692,7 @@ $lang['user']['spam_score_reset'] = "Herstel naar standaardwaarde"; $lang['edit']['spam_policy'] = "Voeg onderdelen toe, of verwijder onderdelen van de witte en zwarte lijst"; $lang['edit']['spam_alias'] = "Maak een nieuw tijdelijk alias aan, of pas deze aan"; +$lang['danger']['comment_too_long'] = "Opmerkingen mogen niet langer dan 160 karakters zijn"; $lang['danger']['img_tmp_missing'] = "Kan afbeelding niet valideren, tijdelijk bestand niet gevonden"; $lang['danger']['img_invalid'] = "Kan afbeelding niet valideren"; $lang['danger']['invalid_mime_type'] = "Ongeldig mime-type"; @@ -699,6 +723,11 @@ $lang['quarantine']['subj'] = "Onderwerp"; $lang['quarantine']['text_plain_content'] = "Inhoud (tekst)"; $lang['quarantine']['text_from_html_content'] = "Inhoud (geconverteerde html)"; $lang['quarantine']['atts'] = "Bijlagen"; +$lang['quarantine']['low_danger'] = "Laag risico"; +$lang['quarantine']['neutral_danger'] = "Neutraal/geen beoordeling"; +$lang['quarantine']['medium_danger'] = "Middelmatig risico"; +$lang['quarantine']['high_danger'] = "Hoog risico"; +$lang['quarantine']['danger'] = "Risico"; $lang['warning']['fuzzy_learn_error'] = "Fuzzy hash training-fout: %s"; $lang['danger']['spam_learn_error'] = "Spamtraining-fout: %s"; $lang['success']['qlearn_spam'] = "Bericht %s werd als spam gemarkeerd en is verwijderd"; @@ -715,7 +744,7 @@ $lang['debug']['external_logs'] = 'Externe logs'; $lang['debug']['static_logs'] = 'Statische logs'; $lang['debug']['solr_uptime'] = 'Uptime'; $lang['debug']['solr_started_at'] = 'Opgestart op'; -$lang['debug']['solr_last_modified'] = 'Laatst bewerkt op'; +$lang['debug']['solr_last_modified'] = 'Voor het laatst bijgewerkt op'; $lang['debug']['solr_size'] = 'Grootte'; $lang['debug']['solr_docs'] = 'Documenten';