Compare commits

..

10 Commits

48 changed files with 3211 additions and 92 deletions

BIN
.metadata

Binary file not shown.

View File

@@ -1,9 +1,8 @@
# This is a shell script excerpt for configuration purposes only.
# Handle with care! Please don't put code here, only variables.
CLASSES_PATTERN="^([^.]*)\..*\.$HOSTNAME\.(dmp|sql\.gz|tgz|log)$"
CLASSES_PATTERN="^([^.]*)\..*\.$HOSTNAME\.(dmp|sql\.gz|tar|tgz|log)$"
DOIT="yes" # if empty the script makes a dry run
# RETAIN_DAYS=7 # retains all files created within that many days
# RETAIN_WEEKS=4 # retains one file per week/month,
# RETAIN_MONTHS=12 # created within that many weeks/months

View File

@@ -0,0 +1,2 @@
# Service-specific maintenance.
*/5 * * * * CRON=1 USER=$LOGNAME $HOME/services/[servicename]/tools/maintenance_cron

View File

View File

@@ -0,0 +1 @@
../storage/volumes/nextcloud_html/config

View File

@@ -0,0 +1,76 @@
# Nextcloud Server (stable) with MariaDB LTS
#
services:
# https://hub.docker.com/_/nextcloud
# https://github.com/docker-library/docs/tree/master/nextcloud
# https://github.com/nextcloud/server
nextcloud:
image: nextcloud:stable-apache
# Database must be accessible.
depends_on:
database:
condition: service_started
restart: unless-stopped
# Take a look the possible public port collision.
ports:
- 8201:80
links:
- database
volumes:
# Needs R/O UID:GID 33:33 (www-data:www-data).
- ./storage/volumes/nextcloud_init_d:/docker-entrypoint-hooks.d:ro
# Needs R/W UID:GID 33:33 (www-data:www-data).
- ./storage/volumes/nextcloud_html:/var/www/html
- ./storage/volumes/nextcloud_data:/var/www/html/data
environment:
MYSQL_HOST: database
MYSQL_DATABASE: nextcloud
MYSQL_USER: nextcloud
# Same as in MariaDB's configuration.
MYSQL_PASSWORD: secret-1
NEXTCLOUD_ADMIN_USER: admin
# E.g. https://www.avast.com/random-password-generator
NEXTCLOUD_ADMIN_PASSWORD: secret-3
# Fill in according to local settings.
#NEXTCLOUD_TRUSTED_DOMAINS: example.com cloud.example.com
# These are required behind a reverse proxy.
APACHE_DISABLE_REWRITE_IP: 1
# Feel free to adapt to your Docker environment.
TRUSTED_PROXIES: 172.16.0.0/12
# If you force https on the reverse proxy (which is recommended).
#OVERWRITEPROTOCOL: https
# Optional context path.
#OVERWRITEWEBROOT: /nextcloud
# PHP settings.
PHP_MEMORY_LIMIT: 1024M
PHP_UPLOAD_LIMIT: 1024M
extra_hosts:
- "host.docker.internal:host-gateway"
labels:
com.centurylinklabs.watchtower.enable: true
#
# https://hub.docker.com/_/mariadb
database:
image: mariadb:lts
restart: unless-stopped
# Only for migration or debugging.
#ports:
# - 3306:3306
volumes:
# Needs R/W UID:GID 999:999.
- ./storage/volumes/mysql:/var/lib/mysql
# Needs read UID 999.
- ./storage/volumes/mysql_conf_d:/etc/mysql/conf.d
- ./storage/volumes/mysql_init_d:/docker-entrypoint-initdb.d
environment:
MARIADB_AUTO_UPGRADE: "1"
MARIADB_INITDB_SKIP_TZINFO: "1"
MYSQL_DATABASE: nextcloud
MYSQL_USER: nextcloud
# E.g. https://www.avast.com/random-password-generator
MYSQL_PASSWORD: secret-1
MYSQL_ROOT_PASSWORD: secret-2
extra_hosts:
- "host.docker.internal:host-gateway"
labels:
com.centurylinklabs.watchtower.enable: true

View File

@@ -0,0 +1,4 @@
# Ignore everything in this directory except these files.
*
!.gitignore
!.rotate_folder.conf

View File

@@ -0,0 +1,8 @@
# This is a shell script excerpt for configuration purposes only.
# Handle with care! Please don't put code here, only variables.
CLASSES_PATTERN="^([^.]*)\..*\.$HOSTNAME\.(dmp|sql\.gz|tgz|tar|log)$"
DOIT="yes" # if empty the script makes a dry run
# RETAIN_DAYS=7 # retains all files created within that many days
# RETAIN_WEEKS=4 # retains one file per week/month,
# RETAIN_MONTHS=12 # created within that many weeks/months

View File

@@ -0,0 +1,3 @@
# Ignore everything in this directory except these files.
*
!.gitignore

View File

@@ -0,0 +1,3 @@
# Ignore everything in this directory except this file.
*
!.gitignore

View File

@@ -0,0 +1,30 @@
# MariaDB 11.x settings according to the Nextcloud stack (optional).
# See also:
# https://gist.github.com/fevangelou/fb72f36bbe333e059b66
# https://github.com/major/MySQLTuner-perl
[mysqld]
# https://gist.github.com/fevangelou/fb72f36bbe333e059b66
# consider https://github.com/major/MySQLTuner-perl
innodb_buffer_pool_size = 2G # 70-80% of memory available for MySQL/MariaDB
innodb_buffer_pool_instances = 2 # Use 1 instance per 1GB of InnoDB pool size
innodb_file_per_table = 1
innodb_flush_log_at_trx_commit = 0
innodb_flush_method = O_DIRECT
innodb_log_buffer_size = 64M
innodb_log_file_size = 256M # should be 25% of innodb_buffer_pool_size / 2
innodb_stats_on_metadata = 0
# https://mariadb.com/kb/en/configuring-mariadb-for-optimal-performance/
# we practically don't use the MyISAM engine
key_buffer_size = 10M
# https://mariadb.com/kb/en/mariadb-memory-allocation/#query-cache
query_cache_type = 0
query_cache_size = 0
#query_cache_type = ON
#query_cache_size = 10M
#query_cache_limit = 1M
# Buffer Settings
#sort_buffer_size = 1M
#join_buffer_size = 1M

View File

@@ -0,0 +1,3 @@
# Ignore everything in this directory except this file.
*
!.gitignore

View File

@@ -0,0 +1,3 @@
# Ignore everything in this directory except this file.
*
!.gitignore

View File

@@ -0,0 +1,3 @@
# Ignore everything in this directory except this file.
*
!.gitignore

View File

@@ -0,0 +1,3 @@
# Ignore everything in this directory except this file.
*
!.gitignore

View File

@@ -0,0 +1,177 @@
#!/bin/bash
#
# A service script to backup the docker-composed MySQL/MariaDB database.
# Dumps database to the $BASE_DIR/storage/backups/dumps folder (by default).
# An optional parameter may change the target folder.
#
# This script gets the database credentials from the docker-compose.yml file
# and calls the mysql_dumpdb worker script which should be installed in
# the same folder or somewhere in the path.
#
# Call as a Docker manager user (member of the docker Linux group) via cron.
#
# Author: Kovács Zoltán <kovacsz@marcusconsulting.hu>
# Kovács Zoltán <kovacs.zoltan@smartfront.hu>
# License: GNU/GPL 3+ https://www.gnu.org/licenses/gpl-3.0.en.html
# 2025-02-26 v0.3
# mod: doesn't tied to a particular composition (Mediawiki, Wordpress, etc).
# 2024-12-01 v0.2.1
# fix: typo in docker-compose version detection.
# 2024-08-25 v0.2
# new: docker-compose v2 compatibility - tested with Ubuntu 24.04 LTS.
# 2021-10-19 v0.1 Initial version.
# Accepted environment variables and their defaults.
#
PAR_BASEDIR=${PAR_BASEDIR:-""} # Service's base folder
PAR_DUMPDIR=${PAR_DUMPDIR:-""} # Folder to dump within
PAR_SERVICE=${PAR_SERVICE:-"database"} # Service's name in composition
# Messages (maybe overridden by configuration).
#
MSG_DOCKERGRPNEED="You must be a member of the docker group."
MSG_DOESNOTRUN="This service doesn't run."
MSG_MISSINGDEP="Fatal: missing dependency"
MSG_MISSINGCONF="Fatal: missing config file"
MSG_MISSINGYML="Fatal: didn't find the docker-compose.yml file"
MSG_NONWRITE="The target directory isn't writable"
MSG_NOLOCATE="Cannot locate the database container."
MSG_NOPARAM="Missing environment parameter"
# Other initialisations.
#
CONFFILE="docker-compose.yml" # Configuration file
DUMPDIR="storage/backups/dumps" # Folder to dump within
USER=${USER:-LOGNAME} # Fix for cron enviroment only
YMLFILE="docker-compose.yml"
# Checks the dependencies.
#
TR=$(which tr 2>/dev/null)
if [ -z "$TR" ]; then echo "$MSG_MISSINGDEP tr."; exit 1 ; fi
for item in basename cat cut date dirname docker \
find grep hostname id pwd tail xargs
do
if [ -n "$(which $item)" ]
then export $(echo $item | "$TR" '[:lower:]' '[:upper:]' | "$TR" '-' '_')=$(which $item)
else echo "$MSG_MISSINGDEP $item." >&2; exit 1; fi
done
# All dependencies are available via "$THECOMMAND" (upper case) call.
#
# Let's find which version of docker-compose is installed.
if [ $($DOCKER compose version >/dev/null 2>&1; echo $?) -eq 0 ]; then
# We'll use v2 if it is available.
DOCKER_COMPOSE="$DOCKER"
commandstring="compose"
else
# Otherwise falling back to v1.
DOCKER_COMPOSE="$(which docker-compose)"
commandstring=""
fi
# One of the two is mandatory.
if [ -z "$DOCKER_COMPOSE" ];then echo "$MSG_MISSINGDEP docker-compose" >&2; exit 1; fi
# Below docker-compose should be called as "$DOCKER_COMPOSE" $commandstring sequence.
# Where I'm?
# https://gist.github.com/TheMengzor/968e5ea87e99d9c41782
SOURCE="$0"
while [ -h "$SOURCE" ]; do
# resolve $SOURCE until the file is no longer a symlink
SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
SOURCE="$("$READLINK" "$SOURCE")"
# if $SOURCE was a relative symlink, we need to resolve it
# relative to the path where the symlink file was located
[[ $SOURCE != /* ]] && SOURCE="$SCRPATH/$SOURCE"
done; SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
# Need to be root or a Docker manager user.
#
[[ "$USER" != 'root' ]] \
&& [[ -z "$(echo "$("$ID" -Gn "$USER") " | "$GREP" ' docker ')" ]] \
&& echo "$MSG_DOCKERGRPNEED" >&2 && exit 1 #"
# Searches the base folder, containing a docker-compose.yml file.
#
# Called from the base folder (./)?
BASE_DIR="$PAR_BASEDIR"
TEST_DIR="$SCRPATH"
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
# Called from ./tools?
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
# Called from ./tools/*.d?
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
# On failure gives it up here.
if [ -z "$BASE_DIR" -o ! -r "$BASE_DIR/$YMLFILE" ]; then
echo "$MSG_MISSINGYML" >&2; exit 1
fi
# Sets the absolute paths.
CONFFILE="$BASE_DIR/$CONFFILE"
DUMPDIR="${PAR_DUMPDIR:-$BASE_DIR/$DUMPDIR}"
# The dump target folder must be writable.
#
[[ ! -w "$DUMPDIR" ]] \
&& echo "$MSG_NONWRITE: $DUMPDIR" >&2 && exit 1
# The composition must be running - silently gives up here if not.
#
[[ -z "$(cd "$BASE_DIR"; "$DOCKER_COMPOSE" $commandstring ps --services --filter "status=running")" ]] \
&& exit 1
# Searches and parses the config file.
#
if [ ! -r "$CONFFILE" ]; then
echo "$MSG_MISSINGCONF $CONFFILE" >&2; exit 1
fi
#
function parse { [[ -z "$1" ]] && return
# Gets the live lines containing the parameter.
value=$("$CAT" "$CONFFILE" | "$GREP" -ve '^#' | \
"$GREP" -e "$1" | "$TR" -d '\r')
# If multiple the last one to consider.
value=$(echo -e "$value" | "$TAIL" -n1)
# Right side of the colon W/O leading and trailing spaces and quotes.
value=$(echo -ne "$value" | "$CUT" -d':' -f2 | "$XARGS")
# Removes the trailing semicolon (if any).
value=${value%;*}
echo -e "$value"; return
}
# All parameters are mandatories.
MYCONTAINER="$PAR_SERVICE" # TODO: guess from the yml
if [ -z "$MYCONTAINER" ]; then echo "$MSG_NOPARAM PAR_SERVICE" >&2; exit 1; fi1; fi
MYDATABASE="$(parse "MYSQL_DATABASE")"
if [ -z "$MYDATABASE" ]; then echo "$MSG_NOPARAM MYSQL_DATABASE" >&2; exit 1; fi
MYUSER="$(parse "MYSQL_USER")"
if [ -z "$MYUSER" ]; then echo "$MSG_NOPARAM MYSQL_USER" >&2; exit 1; fi
MYPASSWORD="$(parse "MYSQL_PASSWORD")"
if [ -z "$MYPASSWORD" ]; then echo "$MSG_NOPARAM MYSQL_PASSWORD" >&2; exit 1; fi
# We've the configuration parsed.
# Converts the database service name to an actual running container's name.
#
MYCONTAINER="$("$DOCKER" inspect -f '{{.Name}}' $(cd "$BASE_DIR"; "$DOCKER_COMPOSE" $commandstring ps -q "$MYCONTAINER") | "$CUT" -c2-)"
# Gives up here if failed.
if [ -z "$MYCONTAINER" ]; then echo "$MSG_NOLOCATE" >&2; exit 1; fi
# Locates the worker script (in PATH or in this script's folder).
#
MYSQL_DUMPDB=$(which mysql_dumpdb)
if [ -z "$MYSQL_DUMPDB" -a -x "$("$DIRNAME" "$0")/mysql_dumpdb" ]; then
MYSQL_DUMPDB="$SCRPATH/mysql_dumpdb"
fi
if [ -z "$MYSQL_DUMPDB" ]; then echo "$MSG_MISSINGDEP mysql_dumpdb."; exit 1 ; fi
# Tries the DB backup.
#
if [ -n "$MYSQL_DUMPDB" -a -w "$DUMPDIR" ]; then
BACKUP_NAME=$MYDATABASE.$("$DATE" '+%Y%m%d_%H%M%S').$("$HOSTNAME")
( cd "$DUMPDIR"
export MYCONTAINER MYUSER MYPASSWORD
"$MYSQL_DUMPDB" --compress "$MYDATABASE" "$DUMPDIR/$BACKUP_NAME.sql" \
2>>"$DUMPDIR/$BACKUP_NAME.log"
)
fi
# That's all, Folks! :)

View File

@@ -0,0 +1,132 @@
#!/bin/bash
#
# A service script to backup the relevant user's storage (apps, data, themes)
# of a docker-composed Nextcloud instance. Creates a tarball in
# $BASE_DIR/storage/backups/tarballs folder (by default). An optional
# parameter may change the target folder.
#
# Call as a Docker manager user (member of the docker Linux group) via cron.
#
# Author: Kovács Zoltán <kovacsz@marcusconsulting.hu>
# License: GNU/GPL 3+ https://www.gnu.org/licenses/gpl-3.0.en.html
# 2026-02-07 v0.2
# new: Excludes the documents if the storage_gitbackup is active.
# mod: custom_apps and themes folders have been added.
# 2025-11-24 v0.1 Initial version.
# Accepted environment variables and their defaults.
#
PAR_BASEDIR=${PAR_BASEDIR:-""} # Service's base folder
PAR_BACKUPDIR=${PAR_BACKUPDIR:-""} # Folder to dump within
# Messages (maybe overridden by configuration).
#
MSG_DOCKERGRPNEED="You must be a member of the docker group."
MSG_DOESNOTRUN="This service doesn't run."
MSG_MISSINGDEP="Fatal: missing dependency"
MSG_MISSINGYML="Fatal: didn't find the docker-compose.yml file"
MSG_NONWRITE="The target directory isn't writable"
MSG_NOLOCATE="Cannot locate the Nextcloud container."
# Other initialisations.
#
BACKUPDIR="storage/backups/tarballs" # Folder to dump within
GITBACKUP="storage_gitbackup.sh" # Git backup utility
SERVICENAME="nextcloud" # The composed Nextcloud service
USER=${USER:-LOGNAME} # Fix for cron enviroment only
YMLFILE="docker-compose.yml"
# Checks the dependencies.
#
TR=$(which tr 2>/dev/null)
if [ -z "$TR" ]; then echo "$MSG_MISSINGDEP tr."; exit 1 ; fi
for item in basename cat cut date dirname docker \
find grep hostname id pwd tail xargs
do
if [ -n "$(which $item)" ]
then export $(echo $item | "$TR" '[:lower:]' '[:upper:]' | "$TR" '-' '_')=$(which $item)
else echo "$MSG_MISSINGDEP $item." >&2; exit 1; fi
done
# All dependencies are available via "$THECOMMAND" (upper case) call.
#
# Let's find which version of docker-compose is installed.
if [ $($DOCKER compose version 2>&1 >/dev/null; echo $?) -eq 0 ]; then
# We'll use v2 if it is available.
DOCKER_COMPOSE="$DOCKER"
commandstring="compose"
else
# Otherwise falling back to v1.
DOCKER_COMPOSE="$(which docker-compose)"
commandstring=""
fi
# One of the two is mandatory.
if [ -z "$DOCKER_COMPOSE" ];then echo "$MSG_MISSINGDEP docker-compose" >&2; exit 1; fi
# Below docker-compose should be called as "$DOCKER_COMPOSE" $commandstring sequence.
# Where I'm?
# https://gist.github.com/TheMengzor/968e5ea87e99d9c41782
SOURCE="$0"
while [ -h "$SOURCE" ]; do
# resolve $SOURCE until the file is no longer a symlink
SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
SOURCE="$("$READLINK" "$SOURCE")"
# if $SOURCE was a relative symlink, we need to resolve it
# relative to the path where the symlink file was located
[[ $SOURCE != /* ]] && SOURCE="$SCRPATH/$SOURCE"
done; SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
# Need to be root or a Docker manager user.
#
[[ "$USER" != 'root' ]] \
&& [[ -z "$(echo "$("$ID" -Gn "$USER") " | "$GREP" ' docker ')" ]] \
&& echo "$MSG_DOCKERGRPNEED" >&2 && exit 1 #"
# Searches the base folder, containing a docker-compose.yml file.
#
# Called from the base folder (./)?
BASE_DIR="$PAR_BASEDIR"
TEST_DIR="$SCRPATH"
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
# Called from ./tools?
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
# Called from ./tools/*.d?
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
# On failure gives it up here.
if [ -z "$BASE_DIR" -o ! -r "$BASE_DIR/$YMLFILE" ]; then
echo "$MSG_MISSINGYML" >&2; exit 1
fi
# Sets the absolute paths.
BACKUPDIR="${PAR_BACKUPDIR:-$BASE_DIR/$BACKUPDIR}"
# The dump target folder must be writable.
#
[[ ! -w "$BACKUPDIR" ]] \
&& echo "$MSG_NONWRITE: $BACKUPDIR" >&2 && exit 1
# The service must be running - silently gives up here if not.
#
[[ -z "$(cd "$BASE_DIR"; "$DOCKER_COMPOSE" $commandstring ps --services --filter "status=running")" ]] \
&& exit 1
# Converts the Nextcloud service name to an actual running container's name.
#
NCCONTAINER="$("$DOCKER" inspect -f '{{.Name}}' $(cd "$BASE_DIR"; "$DOCKER_COMPOSE" $commandstring ps -q "$SERVICENAME") | "$CUT" -c2-)"
# Gives up here if failed.
if [ -z "$NCCONTAINER" ]; then echo "$MSG_NOLOCATE" >&2; exit 1; fi
# Excludes the documents if the GITBACKUP is active.
DOCUMENTS="data"
[[ -n $(which "$GITBACKUP") ]] && DOCUMENTS="" # it is an executable somewhere in the path
[[ -x "$SCRPATH/$GITBACKUP" ]] && DOCUMENTS="" # it is an executable in the current directory
# Tries the FS backup.
if [ -w "$BACKUPDIR" ]; then
BACKUP_NAME=$NCCONTAINER.$("$DATE" '+%Y%m%d_%H%M%S').$("$HOSTNAME")
"$DOCKER" exec $NCCONTAINER sh \
-c "cd /var/www/html; tar cz custom_apps $DOCUMENTS themes" \
> "$BACKUPDIR/$BACKUP_NAME.tgz" 2>>"$BACKUPDIR/$BACKUP_NAME.log"
fi
# That's all, Folks! :)

View File

@@ -0,0 +1,161 @@
#!/bin/bash
#
# Humble tool to commit the content of a docker-composed Nextcloud service's
# web files into a git repository to make a daily backup of documents.
# Also makes the repository with a metastore file if doesn't exist yet.
#
# This script called usually by the cron (but indirectly).
# Depends loosely on metastore package, which isn't absolutely necessary,
# but strongly recommended to backup file time attributes and permissions,
# which the git tool doesn't do.
#
# Author: Kovács Zoltán <kovacs.zoltan@smartfront.hu>
# License: GNU/GPL v3+ (https://www.gnu.org/licenses/gpl-3.0.en.html)
# 2025-11-21 v0.1 Initial release
# Accepted environment variables and their defaults.
#
BOTEMAIL=${BOTEMAIL:-"backupbot@example.com"} # Git repo owner's email (fake)
BOTNAME=${BOTNAME:-"Backup Bot"} # Git repo owner's name (fake)
SERVICE_BASE=${PAR_BASEDIR:-""} # Corresponding service's base
GITDIR=${PAR_GITDIR:-""} # Folder containing .git
BACKUPDIR=${PAR_BACKUPDIR:-""} # Folder to backup into git
# Basic environment settings.
#
LANG=C
LC_ALL=C
# We need also the sbin directories.
if ! [[ "$PATH" =~ '/sbin:' ]]; then
PATH="$PATH:/usr/local/sbin:/usr/sbin:/sbin"; fi
# Messages.
#
MSG_GITCOMMIT="Automated backup"
MSG_MISSINGBASE="Fatal: missing SERVICE_BASE"
MSG_MISSINGDEP="Fatal: missing dependency"
MSG_MISSINGGIT="Fatal: unable to find the backup (git) folder"
MSG_MISSINGSOURCE="Fatal: unable to find the source folder"
MSG_WRONGGIT="Fatal: unusable backup (git) folder"
# Other initialisations.
#
BACKUPPATH="storage/volumes/nextcloud_data"
GITPATH="storage/backups/webcontent"
YMLFILE="docker-compose.yml"
# Checks the dependencies.
#
TR=$(which tr 2>/dev/null)
if [ -z "$TR" ]; then echo "$MSG_MISSINGDEP tr."; exit 1 ; fi
for item in cut date dirname docker git readlink
do
if [ -n "$(which $item)" ]
then export $(echo $item | "$TR" '[:lower:]' '[:upper:]')=$(which $item)
else echo "$MSG_MISSINGDEP $item." >&2; exit 1; fi
done
# All dependencies are available via "$THECOMMAND" (upper case) call.
# Where I'm?
# https://gist.github.com/TheMengzor/968e5ea87e99d9c41782
#
SOURCE="$0"
while [ -h "$SOURCE" ]; do
# resolve $SOURCE until the file is no longer a symlink
SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
SOURCE="$("$READLINK" "$SOURCE")"
# if $SOURCE was a relative symlink, we need to resolve it
# relative to the path where the symlink file was located
[[ $SOURCE != /* ]] && SOURCE="$SCRPATH/$SOURCE"
done; SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
# Let's find which version of docker-compose is installed.
if [ $($DOCKER compose version >/dev/null 2>&1; echo $?) -eq 0 ]; then
# We'll use v2 if it is available.
DOCKER_COMPOSE="$DOCKER"
commandstring="compose"
else
# Otherwise falling back to v1.
DOCKER_COMPOSE="$(which docker-compose)"
commandstring=""
fi
# One of the two is mandatory.
if [ -z "$DOCKER_COMPOSE" ];then echo "$MSG_MISSINGDEP docker-compose" >&2; exit 1; fi
# Below docker-compose should be called as "$DOCKER_COMPOSE" $commandstring sequence.
# Searches the base folder, containing a docker-compose.yml file.
#
# Called from the base folder (./)?
TEST_DIR="$SCRPATH"
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
# Called from ./tools?
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
# Called from ./tools/*.d?
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
# On failure gives it up here.
if [ -z "$SERVICE_BASE" -o ! -r "$SERVICE_BASE/$YMLFILE" ]; then
echo "$MSG_MISSINGYML" >&2; exit 1
fi
# Locates the folder to backup.
#
# Maybe given as a command line parameter.
[[ -n "$1" ]] && SOURCEDIR="$1" && shift
# Or as an environment variable, or fallbacks to the default.
[[ -z "$SOURCEDIR" ]] && SOURCEDIR="${BACKUPDIR:-$SERVICE_BASE/$BACKUPPATH}"
# Gives up here if doesn't found.
if [ -z "$SOURCEDIR" -o ! -d "$("$READLINK" -e "$SOURCEDIR")" ]; then
echo "$MSG_MISSINGSOURCE $SOURCEDIR"; exit 1
fi
# Locates the backup (git) folder.
#
# Maybe given as a command line parameter.
[[ -n "$1" ]] && GITDIR="$1" && shift
# Or fallbacks to the default.
[[ -z "$GITDIR" ]] && GITDIR="$SERVICE_BASE/$GITPATH"
# Gives up here if doesn't found.
if [ -z "$GITDIR" -o ! -d "$("$READLINK" -e "$GITDIR")" ]; then
echo "$MSG_MISSINGGIT $GITDIR"; exit 1
fi
# Does it writable?
( cd "$GITDIR" 2>/dev/null
if [ ! "$PWD" = "$GITDIR" -o ! -w "$PWD" ]; then
echo "$MSG_WRONGGIT $GITDIR"; exit 1
fi
) || exit 1
# We've the folders localized.
# The service must be running - silently gives up here if not.
#
[[ -z "$(cd "$SERVICE_BASE"; "$DOCKER_COMPOSE" $commandstring ps --services --filter "status=running")" ]] \
&& exit 1
# Attempts the backup commit.
#
# Initializes the git backup if it doesn't exist yet.
if [ ! -d "$GITDIR/.git" ]; then
# Initializes the repo itself.
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" init --quiet
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" config user.name "$BOTNAME"
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" config user.email "$BOTEMAIL"
fi
# Stages all the files and non-empty folders.
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" add . >/dev/null
# Stores the file system metadata as well, if the tool has been installed.
if [ ! -z "$(which metastore)" -a -x "$(which metastore)" ]; then
# This commamd silently creates the metastore file if it doesnt' exist yet.
( cd "$SOURCEDIR"
"$(which metastore)" -smqq --file ".metadata"
)
# Stages it as well.
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" add ".metadata" >/dev/null
fi
# Makes the commit.
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" commit --quiet -m "'$MSG_GITCOMMIT $("$DATE" '+%Y%m%d-%H%M%S')'"
# Git done.
# That's all, Folks! :)

View File

@@ -0,0 +1,83 @@
#!/bin/bash
#
# Periodic maintenance operations for a Nextcloud instance.
# This script is usually called by cron (perhaps indirectly).
#
# Author: Kovács Zoltán <kovacsz@marcusconsulting.hu>
# License: GNU/GPL v3+ (https://www.gnu.org/licenses/gpl-3.0.en.html)
# 2025-11-21 v0.1 Initial release
# Accepted environment variables and their defaults.
PAR_BASEDIR=${PAR_BASEDIR:-""} # Service's base folder.
# Other initialisations.
COMMANDLINE="php /var/www/html/cron.php"
COMMANDUSER="www-data"
SERVICENAME="nextcloud"
YMLFILE="docker-compose.yml"
# Messages.
MSG_MISSINGDEP="Fatal: missing dependency"
MSG_MISSINGYML="Fatal: didn't find the docker-compose.yml file"
# Checks the dependencies.
TR=$(which tr 2>/dev/null)
if [ -z "$TR" ]; then echo "$MSG_MISSINGDEP tr."; exit 1 ; fi
for item in basename dirname docker readlink
do
if [ -n "$(which $item)" ]
then export $(echo $item | "$TR" '[:lower:]' '[:upper:]' | "$TR" '-' '_')=$(which $item)
else echo "$MSG_MISSINGDEP $item." >&2; exit 1; fi
done
# All dependencies are available via "$THECOMMAND" (upper case) call.
#
# Let's find which version of docker-compose is installed.
if [ $($DOCKER compose version 2>&1 >/dev/null; echo $?) -eq 0 ]; then
# We'll use v2 if it is available.
DOCKER_COMPOSE="$DOCKER"
commandstring="compose"
else
# Otherwise falling back to v1.
DOCKER_COMPOSE="$(which docker-compose)"
commandstring=""
fi
# One of the two is mandatory.
if [ -z "$DOCKER_COMPOSE" ];then echo "$MSG_MISSINGDEP docker-compose" >&2; exit 1; fi
# Below docker-compose should be called as "$DOCKER_COMPOSE" $commandstring sequence.
# Where I'm?
# https://gist.github.com/TheMengzor/968e5ea87e99d9c41782
SOURCE="$0"
while [ -h "$SOURCE" ]; do
# resolve $SOURCE until the file is no longer a symlink
SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && pwd )" #"
SOURCE="$("$READLINK" "$SOURCE")"
# if $SOURCE was a relative symlink, we need to resolve it
# relative to the path where the symlink file was located
[[ $SOURCE != /* ]] && SOURCE="$SCRPATH/$SOURCE"
done; SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && pwd )" #"
# Searches the base folder, containing a docker-compose.yml file.
# Called from the base folder (./)?
BASE_DIR="$PAR_BASEDIR"
TEST_DIR="$SCRPATH"
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
# Called from ./tools?
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
# Called from ./tools/*.d?
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
# On failure gives it up here silently.
if [ -z "$BASE_DIR" -o ! -r "$BASE_DIR/$YMLFILE" ]; then
echo "$MSG_MISSINGYML" >&2; exit 1
fi
# Only if the service is running.
if [ -n "$(cd "$BASE_DIR"; "$DOCKER_COMPOSE" $commandstring ps --services --filter "status=running")" ]; then
# Runs command COMMANDLINE on service SERVICENAME as COMMANDUSER.
SERVICENAME="$("$BASENAME" "$BASE_DIR")-$SERVICENAME-1"
"$DOCKER" exec -u "$COMMANDUSER" "$SERVICENAME" $COMMANDLINE
fi
# That's all, Folks!

View File

@@ -0,0 +1,195 @@
#!/bin/bash
#
# Restores a composed MySQL/MariaDB database from a dump file.
# Gets all necessary data from the docker-compose.yml file.
#
# This is a wrapper script to the system-wide mysql_restoredb tool.
# Database recovey with the necessary user management and grants
# requires superuser privileges in MySQL, but simple data recovery
# is possible if the user and privileges are already set.
#
# You have to call this script as a Docker manager user (member of the
# 'docker' Linux group). The worker tool must be available somewhere
# in PATH. At least 5.7.6 MySQL or at least 10.1.3 MariaDB is required.
#
# Usage:
# $0 path_to_the_dumpfile [ path_to_the_service's_base ]
#
# Author: Kovács Zoltán <kovacsz@marcusconsulting.hu>
# License: GNU/GPL v3+ (https://www.gnu.org/licenses/gpl-3.0.en.html)
#
# 2025-02-26 v0.1 Forked from the Smartfront repository and rewritten.
# Accepted environment variables and their defaults.
#
PAR_SERVICE=${SERVICE:-"database"} # Database container's name
# Other initialisations.
#
BACKUPFOLDER="storage/backups/dumps" # Skeleton's default dump folder
PROP_DBAPASS="MYSQL_ROOT_PASSWORD" # DB admin password property
PROP_DBNAME="MYSQL_DATABASE" # DB name property
PROP_DBPASS="MYSQL_PASSWORD" # DB password property
PROP_DBUSER="MYSQL_USER" # DB username property
USER=${USER:-LOGNAME} # Fix for cron enviroment only
YMLFILE="docker-compose.yml"
# Basic environment settings.
#
LANG=C
LC_ALL=C
# Messages.
#
MSG_BADDUMP="Fatal: doesn't exist or doesn't a dumpfile:"
MSG_DOCKERGRPNEED="You must be a member of the docker group."
MSG_DOESNOTRUN="This service doesn't run."
MSG_MISSINGDEP="Fatal: missing dependency"
MSG_MISSINGCONF="Fatal: missing config file"
MSG_MISSINGYML="Fatal: didn't find the $YMLFILE file"
MSG_NOLOCATE="Cannot locate the database container."
MSG_NOPARAM="Missing environment parameter"
MSG_USAGE="Usage: $0 dump_pathname [ composition_base_pathname ]\n"
MSG_USAGE+="ENVVAR:\n"
MSG_USAGE+="SERVICE \tDatabase service's name in composition\n"
# Checks the dependencies.
#
TR=$(which tr 2>/dev/null)
if [ -z "$TR" ]; then echo "$MSG_MISSINGDEP tr."; exit 1 ; fi
for item in basename cat cut date dirname docker \
grep id mysql_restoredb readlink tail xargs
do
if [ -n "$(which $item)" ]
then export $(echo $item | "$TR" '[:lower:]' '[:upper:]' | "$TR" '-' '_')=$(which $item)
else echo "$MSG_MISSINGDEP $item." >&2; exit 1; fi
done
# All dependencies are available via "$THECOMMAND" (upper case) call.
#
# Let's find which version of docker-compose is installed.
if [ $($DOCKER compose version >/dev/null 2>&1; echo $?) -eq 0 ]; then
# We'll use v2 if it is available.
DOCKER_COMPOSE="$DOCKER"
commandstring="compose"
else
# Otherwise falling back to v1.
DOCKER_COMPOSE="$(which docker-compose)"
commandstring=""
fi
# One of the two is mandatory.
if [ -z "$DOCKER_COMPOSE" ];then echo "$MSG_MISSINGDEP docker-compose" >&2; exit 1; fi
# Below docker-compose should be called as "$DOCKER_COMPOSE" $commandstring sequence.
# Where I'm?
# https://gist.github.com/TheMengzor/968e5ea87e99d9c41782
SOURCE="$0"
while [ -h "$SOURCE" ]; do
# resolve $SOURCE until the file is no longer a symlink
SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
SOURCE="$("$READLINK" "$SOURCE")"
# if $SOURCE was a relative symlink, we need to resolve it
# relative to the path where the symlink file was located
[[ $SOURCE != /* ]] && SOURCE="$SCRPATH/$SOURCE"
done; SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
# Need to be root or a Docker manager user.
#
[[ "$USER" != 'root' ]] \
&& [[ -z "$(echo "$("$ID" -Gn "$USER") " | "$GREP" ' docker ')" ]] \
&& echo "$MSG_DOCKERGRPNEED" >&2 && exit 1 #"
# Gets the command line parameters.
#
# DUMPFILE is mandatory
if [ -n "$1" ]; then DUMPFILE="$1"; shift
else echo -e "$MSG_USAGE" >&2; exit 1; fi
# SERVICE_BASE is optional
if [ -n "$1" ]; then SERVICE_BASE="$1"; shift; fi
# We've read the unchecked command line parameters.
# Searches the base folder, containing the YMLFILE.
#
if [ -z "$SERVICE_BASE" ]; then
# Called from the base folder (./)?
TEST_DIR="$SCRPATH"
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
# Called from ./tools?
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
# Called from ./tools/*.d?
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
fi
# On failure gives it up here.
if [ -z "$SERVICE_BASE" -o ! -r "$SERVICE_BASE/$YMLFILE" ]; then
echo "$MSG_MISSINGYML" >&2; exit 1
fi
# Sets the absolute path.
YMLFILE="$SERVICE_BASE/$YMLFILE"
# We've the YMLFILE.
# Finds the DUMPFILE to use.
#
# The DUMPFILE must point to a readable file.
# If doesn't it tries the skeleton's standard backup folder as well.
if [ ! -r "$DUMPFILE" ]
then DUMPFILE="$("$DIRNAME" "$SERVICE_BASE")/$BACKUPFOLDER/$DUMPFILE"; fi
# If it is an existing symlink dereferences it to ensure, it points to a file.
if [ -h "$DUMPFILE" ]; then
if [[ "$("$READLINK" "$DUMPFILE")" != /* ]]
# relative path in symlink
then DUMPFILE="$("$DIRNAME" "$DUMPFILE")/$("$READLINK" "$DUMPFILE")"
# absolute path in symlink
else DUMPFILE="$("$READLINK" "$DUMPFILE")"; fi
fi
# Let's check it!
if [ ! -r "$DUMPFILE" -o ! -f "$DUMPFILE" ]
then echo -e "$MSG_BADDUMP $DUMPFILE"; exit 1; fi
# We've an existing dumpfile.
# The composition must be running - silently gives up here if not.
#
[[ -z "$(cd "$SERVICE_BASE"; "$DOCKER_COMPOSE" $commandstring ps --services --filter "status=running")" ]] \
&& exit 1
# Parses the YMLFILE for parameters to use.
#
function parse { [[ -z "$1" ]] && return
# Gets the live lines containing the parameter.
value=$("$CAT" "$YMLFILE" | "$GREP" -ve '^#' | \
"$GREP" -e "^ *$1" | "$TR" -d '\r')
# If multiple the last one to consider.
value=$(echo -e "$value" | "$TAIL" -n1)
# Right side of the colon W/O leading and trailing spaces and quotes.
value=$(echo -ne "$value" | "$CUT" -d':' -f2 | "$XARGS")
# Removes the trailing semicolon (if any).
value=${value%;*}
echo -e "$value"; return
}
# These parameters are mandatory.
MYCONTAINER="$PAR_SERVICE" # TODO: guess from the yml
if [ -z "$MYCONTAINER" ]; then echo "$MSG_NOPARAM PAR_SERVICE" >&2; exit 1; fi1; fi
MYDATABASE="$(parse "$PROP_DBNAME")"
if [ -z "$MYDATABASE" ]; then echo "$MSG_NOPARAM $PROP_DBNAME" >&2; exit 1; fi
MYUSER="$(parse "$PROP_DBUSER")"
if [ -z "$MYUSER" ]; then echo "$MSG_NOPARAM $PROP_DBUSER" >&2; exit 1; fi
MYPASSWORD="$(parse "$PROP_DBPASS")"
if [ -z "$MYPASSWORD" ]; then echo "$MSG_NOPARAM $PROP_DBPASS" >&2; exit 1; fi
# These are optional.
MYDBAUSER="root"
MYDBAPASSWORD="$(parse "$PROP_DBAPASS")"
# We've the configuration parsed.
# Converts the database service name to an actual running container's name.
#
MYCONTAINER="$("$DOCKER" inspect -f '{{.Name}}' $(cd "$SERVICE_BASE"; "$DOCKER_COMPOSE" $commandstring ps -q "$MYCONTAINER") | "$CUT" -c2-)"
# Gives up here if failed.
if [ -z "$MYCONTAINER" ]; then echo "$MSG_NOLOCATE" >&2; exit 1; fi
# Calls the worker script to make the job.
#
export MYDBAUSER MYDBAPASSWORD MYPASSWORD
"$MYSQL_RESTOREDB" -C "$MYCONTAINER" -U "$MYUSER" "$MYDATABASE" "$DUMPFILE"
# That's all, Folks! :)

View File

View File

@@ -0,0 +1,29 @@
# = Redmine configuration file
# The entrypoint script sets chown 999:999 and chmod 644 to this file.
#
# Each environment has its own configuration options. If you are only
# running in production, only the production block needs to be configured.
# Environment specific configuration options override the default ones.
#
# Note that this file needs to be a valid YAML file.
# DO NOT USE TABS! Use 2 spaces instead of tabs for indentation.
# default configuration options for all environments
default:
# Outgoing emails configuration
# Feel free to setup your local SMTP settings here.
email_delivery:
delivery_method: :smtp
smtp_settings:
address: 'host.docker.internal'
port: 25
enable_starttls_auto: false
# specific configuration options for production environment
# that overrides the default ones
production:
# specific configuration options for development environment
# that overrides the default ones
development:

View File

@@ -0,0 +1,21 @@
# = Redmine database configuration file
# The entrypoint script sets chown 999:999 and chmod 644 to this file.
#
# Note that this file needs to be a valid YAML file.
# DO NOT USE TABS! Use 2 spaces instead of tabs for indentation.
production:
adapter: mysql2
database: redmine
host: database
username: redmine
# It should be the same as in the docker-compose.yml file.
password: "secret-1"
# Use "utf8" instead of "utfmb4" for MySQL prior to 5.7.7
encoding: utf8mb4
variables:
# Recommended `transaction_isolation` for MySQL to avoid concurrency issues is
# `READ-COMMITTED`.
# In case of MySQL lower than 8, the variable name is `tx_isolation`.
# See https://www.redmine.org/projects/redmine/wiki/MySQL_configuration
transaction_isolation: "READ-COMMITTED"

View File

@@ -0,0 +1 @@
../storage/volumes/mysql_conf_d

View File

@@ -0,0 +1,84 @@
# Redmine (optionally with LDAP authentication) with MariaDB.
#
services:
# https://hub.docker.com/_/redmine
# https://github.com/docker-library/redmine
# https://github.com/redmine/redmine
redmine:
image: redmine:6
# Database must be accessible.
depends_on:
database:
condition: service_started
# Below you may find a possible solution if you also need to install some of the plugins'
# opsys dependencies. We don't want to extend the official image to maintain watchtower's
# monitoring for updates. So we use CMD to make all the necessary changes.
# This will slightly prolong the start of the service.
# command:
# - /bin/bash
# - -c
# - |
# DEBIAN_FRONTEND=noninteractive apt update
# apt install -y --no-install-recommends build-essential zlib1g-dev
# apt clean
# rm -rf /var/lib/apt/lists/*
# source /docker-entrypoint.sh rails server -b 0.0.0.0
restart: unless-stopped
# Take a look the possible public port collision.
ports:
- 8201:3000
links:
- database
volumes:
- /etc/localtime:/etc/localtime:ro
# The entrypoint script sets chown 999:999 and chmod 755/644 to these below.
# https://www.redmine.org/projects/redmine/wiki/RedmineInstall#Step-8-File-system-permissions
- ./storage/volumes/redmine_files:/usr/src/redmine/files
- ./configs/configuration.yml:/usr/src/redmine/config/configuration.yml
- ./configs/database.yml:/usr/src/redmine/config/database.yml
# Needs read UID 999.
- ./storage/volumes/redmine_plugins:/usr/src/redmine/plugins
environment:
REDMINE_DB_MYSQL: database
REDMINE_DB_DATABASE: redmine
REDMINE_DB_USERNAME: redmine
# Same as MYSQL_PASSWORD below.
REDMINE_DB_PASSWORD: secret-1
# Only if you want to disable internal DB version updates.
#REDMINE_NO_DB_MIGRATE: 'yes'
# Automatically follows DB version updates related to the plugins.
REDMINE_PLUGINS_MIGRATE: 'yes'
# https://www.avast.com/random-password-generator
REDMINE_SECRET_KEY_BASE: secret-3
extra_hosts:
- "host.docker.internal:host-gateway"
labels:
com.centurylinklabs.watchtower.enable: true
#
# https://hub.docker.com/_/mariadb
database:
image: mariadb:lts
restart: unless-stopped
# Only for migration or debugging.
#ports:
# - 3306:3306
volumes:
- /etc/localtime:/etc/localtime:ro
# Needs R/W UID:GID 999:999.
- ./storage/volumes/mysql:/var/lib/mysql
# Needs read UID 999.
- ./storage/volumes/mysql_conf_d:/etc/mysql/conf.d:ro
- ./storage/volumes/mysql_init_d:/docker-entrypoint-initdb.d:ro
environment:
MARIADB_AUTO_UPGRADE: "1"
MARIADB_INITDB_SKIP_TZINFO: "1"
MYSQL_DATABASE: redmine
MYSQL_USER: redmine
# https://www.avast.com/random-password-generator
MYSQL_PASSWORD: secret-1
MYSQL_ROOT_PASSWORD: secret-2
extra_hosts:
- "host.docker.internal:host-gateway"
labels:
com.centurylinklabs.watchtower.enable: true

View File

@@ -0,0 +1,3 @@
# Ignore everything in this directory except these files.
*
!.gitignore

View File

@@ -0,0 +1,4 @@
# Ignore everything in this directory except these files.
*
!.gitignore
!.rotate_folder.conf

View File

@@ -0,0 +1,8 @@
# This is a shell script excerpt for configuration purposes only.
# Handle with care! Please don't put code here, only variables.
CLASSES_PATTERN="^([^.]*)\..*\.$HOSTNAME\.(dmp|sql\.gz|tar|tgz|log)$"
DOIT="yes" # if empty the script makes a dry run
# RETAIN_DAYS=7 # retains all files created within that many days
# RETAIN_WEEKS=4 # retains one file per week/month,
# RETAIN_MONTHS=12 # created within that many weeks/months

View File

@@ -0,0 +1,3 @@
# Ignore everything in this directory except these files.
*
!.gitignore

View File

@@ -0,0 +1,3 @@
# Ignore everything in this directory except this file.
*
!.gitignore

View File

@@ -0,0 +1,30 @@
# MariaDB 11.x settings according to the Redmine stack (optional).
# See also:
# https://gist.github.com/fevangelou/fb72f36bbe333e059b66
# https://github.com/major/MySQLTuner-perl
[mysqld]
# https://gist.github.com/fevangelou/fb72f36bbe333e059b66
# consider https://github.com/major/MySQLTuner-perl
innodb_buffer_pool_size = 2G # 70-80% of memory available for MySQL/MariaDB
innodb_buffer_pool_instances = 2 # Use 1 instance per 1GB of InnoDB pool size
innodb_file_per_table = 1
innodb_flush_log_at_trx_commit = 0
innodb_flush_method = O_DIRECT
innodb_log_buffer_size = 64M
innodb_log_file_size = 256M # should be 25% of innodb_buffer_pool_size / 2
innodb_stats_on_metadata = 0
# https://mariadb.com/kb/en/configuring-mariadb-for-optimal-performance/
# we practically don't use the MyISAM engine
key_buffer_size = 10M
# https://mariadb.com/kb/en/mariadb-memory-allocation/#query-cache
query_cache_type = 0
query_cache_size = 0
#query_cache_type = ON
#query_cache_size = 10M
#query_cache_limit = 1M
# Buffer Settings
#sort_buffer_size = 1M
#join_buffer_size = 1M

View File

@@ -0,0 +1,3 @@
# Ignore everything in this directory except this file.
*
!.gitignore

View File

@@ -0,0 +1,3 @@
# Ignore everything in this directory except this file.
*
!.gitignore

View File

@@ -0,0 +1,6 @@
#!/bin/bash
chown -R 999:999 *
find . -type d -exec chmod 2770 {} \;
find . -type f -exec chmod 660 {} \;

View File

@@ -0,0 +1,177 @@
#!/bin/bash
#
# A service script to backup the docker-composed MySQL/MariaDB database.
# Dumps database to the $BASE_DIR/storage/backups/dumps folder (by default).
# An optional parameter may change the target folder.
#
# This script gets the database credentials from the docker-compose.yml file
# and calls the mysql_dumpdb worker script which should be installed in
# the same folder or somewhere in the path.
#
# Call as a Docker manager user (member of the docker Linux group) via cron.
#
# Author: Kovács Zoltán <kovacsz@marcusconsulting.hu>
# Kovács Zoltán <kovacs.zoltan@smartfront.hu>
# License: GNU/GPL 3+ https://www.gnu.org/licenses/gpl-3.0.en.html
# 2025-02-26 v0.3
# mod: doesn't tied to a particular composition (Mediawiki, Wordpress, etc).
# 2024-12-01 v0.2.1
# fix: typo in docker-compose version detection.
# 2024-08-25 v0.2
# new: docker-compose v2 compatibility - tested with Ubuntu 24.04 LTS.
# 2021-10-19 v0.1 Initial version.
# Accepted environment variables and their defaults.
#
PAR_BASEDIR=${PAR_BASEDIR:-""} # Service's base folder
PAR_DUMPDIR=${PAR_DUMPDIR:-""} # Folder to dump within
PAR_SERVICE=${PAR_SERVICE:-"database"} # Service's name in composition
# Messages (maybe overridden by configuration).
#
MSG_DOCKERGRPNEED="You must be a member of the docker group."
MSG_DOESNOTRUN="This service doesn't run."
MSG_MISSINGDEP="Fatal: missing dependency"
MSG_MISSINGCONF="Fatal: missing config file"
MSG_MISSINGYML="Fatal: didn't find the docker-compose.yml file"
MSG_NONWRITE="The target directory isn't writable"
MSG_NOLOCATE="Cannot locate the database container."
MSG_NOPARAM="Missing environment parameter"
# Other initialisations.
#
CONFFILE="docker-compose.yml" # Configuration file
DUMPDIR="storage/backups/dumps" # Folder to dump within
USER=${USER:-LOGNAME} # Fix for cron enviroment only
YMLFILE="docker-compose.yml"
# Checks the dependencies.
#
TR=$(which tr 2>/dev/null)
if [ -z "$TR" ]; then echo "$MSG_MISSINGDEP tr."; exit 1 ; fi
for item in basename cat cut date dirname docker \
find grep hostname id pwd tail xargs
do
if [ -n "$(which $item)" ]
then export $(echo $item | "$TR" '[:lower:]' '[:upper:]' | "$TR" '-' '_')=$(which $item)
else echo "$MSG_MISSINGDEP $item." >&2; exit 1; fi
done
# All dependencies are available via "$THECOMMAND" (upper case) call.
#
# Let's find which version of docker-compose is installed.
if [ $($DOCKER compose version >/dev/null 2>&1; echo $?) -eq 0 ]; then
# We'll use v2 if it is available.
DOCKER_COMPOSE="$DOCKER"
commandstring="compose"
else
# Otherwise falling back to v1.
DOCKER_COMPOSE="$(which docker-compose)"
commandstring=""
fi
# One of the two is mandatory.
if [ -z "$DOCKER_COMPOSE" ];then echo "$MSG_MISSINGDEP docker-compose" >&2; exit 1; fi
# Below docker-compose should be called as "$DOCKER_COMPOSE" $commandstring sequence.
# Where I'm?
# https://gist.github.com/TheMengzor/968e5ea87e99d9c41782
SOURCE="$0"
while [ -h "$SOURCE" ]; do
# resolve $SOURCE until the file is no longer a symlink
SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
SOURCE="$("$READLINK" "$SOURCE")"
# if $SOURCE was a relative symlink, we need to resolve it
# relative to the path where the symlink file was located
[[ $SOURCE != /* ]] && SOURCE="$SCRPATH/$SOURCE"
done; SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
# Need to be root or a Docker manager user.
#
[[ "$USER" != 'root' ]] \
&& [[ -z "$(echo "$("$ID" -Gn "$USER") " | "$GREP" ' docker ')" ]] \
&& echo "$MSG_DOCKERGRPNEED" >&2 && exit 1 #"
# Searches the base folder, containing a docker-compose.yml file.
#
# Called from the base folder (./)?
BASE_DIR="$PAR_BASEDIR"
TEST_DIR="$SCRPATH"
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
# Called from ./tools?
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
# Called from ./tools/*.d?
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
# On failure gives it up here.
if [ -z "$BASE_DIR" -o ! -r "$BASE_DIR/$YMLFILE" ]; then
echo "$MSG_MISSINGYML" >&2; exit 1
fi
# Sets the absolute paths.
CONFFILE="$BASE_DIR/$CONFFILE"
DUMPDIR="${PAR_DUMPDIR:-$BASE_DIR/$DUMPDIR}"
# The dump target folder must be writable.
#
[[ ! -w "$DUMPDIR" ]] \
&& echo "$MSG_NONWRITE: $DUMPDIR" >&2 && exit 1
# The composition must be running - silently gives up here if not.
#
[[ -z "$(cd "$BASE_DIR"; "$DOCKER_COMPOSE" $commandstring ps --services --filter "status=running")" ]] \
&& exit 1
# Searches and parses the config file.
#
if [ ! -r "$CONFFILE" ]; then
echo "$MSG_MISSINGCONF $CONFFILE" >&2; exit 1
fi
#
function parse { [[ -z "$1" ]] && return
# Gets the live lines containing the parameter.
value=$("$CAT" "$CONFFILE" | "$GREP" -ve '^#' | \
"$GREP" -e "$1" | "$TR" -d '\r')
# If multiple the last one to consider.
value=$(echo -e "$value" | "$TAIL" -n1)
# Right side of the colon W/O leading and trailing spaces and quotes.
value=$(echo -ne "$value" | "$CUT" -d':' -f2 | "$XARGS")
# Removes the trailing semicolon (if any).
value=${value%;*}
echo -e "$value"; return
}
# All parameters are mandatories.
MYCONTAINER="$PAR_SERVICE" # TODO: guess from the yml
if [ -z "$MYCONTAINER" ]; then echo "$MSG_NOPARAM PAR_SERVICE" >&2; exit 1; fi1; fi
MYDATABASE="$(parse "MYSQL_DATABASE")"
if [ -z "$MYDATABASE" ]; then echo "$MSG_NOPARAM MYSQL_DATABASE" >&2; exit 1; fi
MYUSER="$(parse "MYSQL_USER")"
if [ -z "$MYUSER" ]; then echo "$MSG_NOPARAM MYSQL_USER" >&2; exit 1; fi
MYPASSWORD="$(parse "MYSQL_PASSWORD")"
if [ -z "$MYPASSWORD" ]; then echo "$MSG_NOPARAM MYSQL_PASSWORD" >&2; exit 1; fi
# We've the configuration parsed.
# Converts the database service name to an actual running container's name.
#
MYCONTAINER="$("$DOCKER" inspect -f '{{.Name}}' $(cd "$BASE_DIR"; "$DOCKER_COMPOSE" $commandstring ps -q "$MYCONTAINER") | "$CUT" -c2-)"
# Gives up here if failed.
if [ -z "$MYCONTAINER" ]; then echo "$MSG_NOLOCATE" >&2; exit 1; fi
# Locates the worker script (in PATH or in this script's folder).
#
MYSQL_DUMPDB=$(which mysql_dumpdb)
if [ -z "$MYSQL_DUMPDB" -a -x "$("$DIRNAME" "$0")/mysql_dumpdb" ]; then
MYSQL_DUMPDB="$SCRPATH/mysql_dumpdb"
fi
if [ -z "$MYSQL_DUMPDB" ]; then echo "$MSG_MISSINGDEP mysql_dumpdb."; exit 1 ; fi
# Tries the DB backup.
#
if [ -n "$MYSQL_DUMPDB" -a -w "$DUMPDIR" ]; then
BACKUP_NAME=$MYDATABASE.$("$DATE" '+%Y%m%d_%H%M%S').$("$HOSTNAME")
( cd "$DUMPDIR"
export MYCONTAINER MYUSER MYPASSWORD
"$MYSQL_DUMPDB" --compress "$MYDATABASE" "$DUMPDIR/$BACKUP_NAME.sql" \
2>>"$DUMPDIR/$BACKUP_NAME.log"
)
fi
# That's all, Folks! :)

View File

@@ -0,0 +1,132 @@
#!/bin/bash
#
# A service script to backup the relevant storage (config, files, plugins)
# of a docker-composed Redmine instance. Creates a tarball in
# $BASE_DIR/storage/backups/tarballs folder (by default). An optional
# parameter may change the target folder.
#
# Call as a Docker manager user (member of the docker Linux group) via cron.
#
# Author: Kovács Zoltán <kovacs.zoltan@smartfront.hu>
# License: GNU/GPL 3+ https://www.gnu.org/licenses/gpl-3.0.en.html
# 2024-09-10 v0.2
# new: Excludes the documents if the storage_gitbackup is active.
# new: docker-compose v2 compatibility - tested with Ubuntu 24.04 LTS.
# 2021-09-01 v0.1 Initial version.
# Accepted environment variables and their defaults.
#
PAR_BASEDIR=${PAR_BASEDIR:-""} # Service's base folder
PAR_BACKUPDIR=${PAR_BACKUPDIR:-""} # Folder to dump within
# Messages (maybe overridden by configuration).
#
MSG_DOCKERGRPNEED="You must be a member of the docker group."
MSG_DOESNOTRUN="This service doesn't run."
MSG_MISSINGDEP="Fatal: missing dependency"
MSG_MISSINGYML="Fatal: didn't find the docker-compose.yml file"
MSG_NONWRITE="The target directory isn't writable"
MSG_NOLOCATE="Cannot locate the Redmine container."
# Other initialisations.
#
BACKUPDIR="storage/backups/tarballs" # Folder to dump within
GITBACKUP="storage_gitbackup.sh" # Git backup utility
SERVICENAME="redmine" # The composed Redmine service
USER=${USER:-LOGNAME} # Fix for cron enviroment only
YMLFILE="docker-compose.yml"
# Checks the dependencies.
#
TR=$(which tr 2>/dev/null)
if [ -z "$TR" ]; then echo "$MSG_MISSINGDEP tr."; exit 1 ; fi
for item in basename cat cut date dirname docker \
find grep hostname id pwd tail xargs
do
if [ -n "$(which $item)" ]
then export $(echo $item | "$TR" '[:lower:]' '[:upper:]' | "$TR" '-' '_')=$(which $item)
else echo "$MSG_MISSINGDEP $item." >&2; exit 1; fi
done
# All dependencies are available via "$THECOMMAND" (upper case) call.
#
# Let's find which version of docker-compose is installed.
if [ $($DOCKER compose version 2>&1 >/dev/null; echo $?) -eq 0 ]; then
# We'll use v2 if it is available.
DOCKER_COMPOSE="$DOCKER"
commandstring="compose"
else
# Otherwise falling back to v1.
DOCKER_COMPOSE="$(which docker-compose)"
commandstring=""
fi
# One of the two is mandatory.
if [ -z "$DOCKER_COMPOSE" ];then echo "$MSG_MISSINGDEP docker-compose" >&2; exit 1; fi
# Below docker-compose should be called as "$DOCKER_COMPOSE" $commandstring sequence.
# Where I'm?
# https://gist.github.com/TheMengzor/968e5ea87e99d9c41782
SOURCE="$0"
while [ -h "$SOURCE" ]; do
# resolve $SOURCE until the file is no longer a symlink
SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
SOURCE="$("$READLINK" "$SOURCE")"
# if $SOURCE was a relative symlink, we need to resolve it
# relative to the path where the symlink file was located
[[ $SOURCE != /* ]] && SOURCE="$SCRPATH/$SOURCE"
done; SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
# Need to be root or a Docker manager user.
#
[[ "$USER" != 'root' ]] \
&& [[ -z "$(echo "$("$ID" -Gn "$USER") " | "$GREP" ' docker ')" ]] \
&& echo "$MSG_DOCKERGRPNEED" >&2 && exit 1 #"
# Searches the base folder, containing a docker-compose.yml file.
#
# Called from the base folder (./)?
BASE_DIR="$PAR_BASEDIR"
TEST_DIR="$SCRPATH"
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
# Called from ./tools?
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
# Called from ./tools/*.d?
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
# On failure gives it up here.
if [ -z "$BASE_DIR" -o ! -r "$BASE_DIR/$YMLFILE" ]; then
echo "$MSG_MISSINGYML" >&2; exit 1
fi
# Sets the absolute paths.
BACKUPDIR="${PAR_BACKUPDIR:-$BASE_DIR/$BACKUPDIR}"
# The dump target folder must be writable.
#
[[ ! -w "$BACKUPDIR" ]] \
&& echo "$MSG_NONWRITE: $BACKUPDIR" >&2 && exit 1
# The service must be running - silently gives up here if not.
#
[[ -z "$(cd "$BASE_DIR"; "$DOCKER_COMPOSE" $commandstring ps --services --filter "status=running")" ]] \
&& exit 1
# Converts the Redmine service name to an actual running container's name.
#
RMCONTAINER="$("$DOCKER" inspect -f '{{.Name}}' $(cd "$BASE_DIR"; "$DOCKER_COMPOSE" $commandstring ps -q "$SERVICENAME") | "$CUT" -c2-)"
# Gives up here if failed.
if [ -z "$RMCONTAINER" ]; then echo "$MSG_NOLOCATE" >&2; exit 1; fi
# Excludes the documents if the GITBACKUP is active.
DOCUMENTS="files"
[[ -n $(which "$GITBACKUP") ]] && DOCUMENTS="" # it is an executable somewhere in the path
[[ -x "$SCRPATH/$GITBACKUP" ]] && DOCUMENTS="" # it is an executable in the current directory
# Tries the FS backup.
if [ -w "$BACKUPDIR" ]; then
BACKUP_NAME=$RMCONTAINER.$("$DATE" '+%Y%m%d_%H%M%S').$("$HOSTNAME")
"$DOCKER" exec $RMCONTAINER sh \
-c "cd /usr/src/redmine; tar cz config $DOCUMENTS plugins" \
> "$BACKUPDIR/$BACKUP_NAME.tgz" 2>>"$BACKUPDIR/$BACKUP_NAME.log"
fi
# That's all, Folks! :)

View File

@@ -0,0 +1,164 @@
#!/bin/bash
#
# Humble tool to commit the content of a docker-composed Redmine service's
# attachments into a git repository to make a daily backup of documents.
# Also makes the repository with a metastore file if doesn't exist yet.
#
# This script called usually by the cron (but indirectly).
# Depends loosely on metastore package, which isn't absolutely necessary,
# but strongly recommended to backup file time attributes and permissions,
# which the git tool doesn't do.
#
# Author: Kovács Zoltán <kovacsz@marcusconsulting.hu>
# License: GNU/GPL v3+ (https://www.gnu.org/licenses/gpl-3.0.en.html)
# 2025-01-18 v0.3
# fix: a typo (BASE_DIR instead of SERVICE_BASE).
# 2024-09-10 v0.2
# new: docker-compose v2 compatibility - tested with Ubuntu 24.04 LTS.
# mod: Doesn't do backup if the service is down.
# 2021-11-21 v0.1 Initial release
# Accepted environment variables and their defaults.
#
BOTEMAIL=${BOTEMAIL:-"backupbot@example.com"} # Git repo owner's email (fake)
BOTNAME=${BOTNAME:-"Backup Bot"} # Git repo owner's name (fake)
SERVICE_BASE=${PAR_BASEDIR:-""} # Corresponding service's base
GITDIR=${PAR_GITDIR:-""} # Folder containing .git
SOURCEDIR=${PAR_BACKUPDIR:-""} # Folder to backup into git
# Basic environment settings.
#
LANG=C
LC_ALL=C
# We need also the sbin directories.
if ! [[ "$PATH" =~ '/sbin:' ]]; then
PATH="$PATH:/usr/local/sbin:/usr/sbin:/sbin"; fi
# Messages.
#
MSG_GITCOMMIT="Automated backup"
MSG_MISSINGBASE="Fatal: missing SERVICE_BASE"
MSG_MISSINGDEP="Fatal: missing dependency"
MSG_MISSINGGIT="Fatal: unable to find the backup (git) folder"
MSG_MISSINGSOURCE="Fatal: unable to find the source folder"
MSG_WRONGGIT="Fatal: unusable backup (git) folder"
# Other initialisations.
#
GITPATH="storage/backups/attachments"
YMLFILE="docker-compose.yml"
# Checks the dependencies.
#
TR=$(which tr 2>/dev/null)
if [ -z "$TR" ]; then echo "$MSG_MISSINGDEP tr."; exit 1 ; fi
for item in cut date dirname docker git readlink
do
if [ -n "$(which $item)" ]
then export $(echo $item | "$TR" '[:lower:]' '[:upper:]')=$(which $item)
else echo "$MSG_MISSINGDEP $item." >&2; exit 1; fi
done
# All dependencies are available via "$THECOMMAND" (upper case) call.
# Where I'm?
# https://gist.github.com/TheMengzor/968e5ea87e99d9c41782
#
SOURCE="$0"
while [ -h "$SOURCE" ]; do
# resolve $SOURCE until the file is no longer a symlink
SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
SOURCE="$("$READLINK" "$SOURCE")"
# if $SOURCE was a relative symlink, we need to resolve it
# relative to the path where the symlink file was located
[[ $SOURCE != /* ]] && SOURCE="$SCRPATH/$SOURCE"
done; SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
# Let's find which version of docker-compose is installed.
if [ $($DOCKER compose version 2>&1 >/dev/null; echo $?) -eq 0 ]; then
# We'll use v2 if it is available.
DOCKER_COMPOSE="$DOCKER"
commandstring="compose"
else
# Otherwise falling back to v1.
DOCKER_COMPOSE="$(which docker-compose)"
commandstring=""
fi
# One of the two is mandatory.
if [ -z "$DOCKER_COMPOSE" ];then echo "$MSG_MISSINGDEP docker-compose" >&2; exit 1; fi
# Below docker-compose should be called as "$DOCKER_COMPOSE" $commandstring sequence.
# Searches the base folder, containing a docker-compose.yml file.
#
# Called from the base folder (./)?
TEST_DIR="$SCRPATH"
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
# Called from ./tools?
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
# Called from ./tools/*.d?
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
# On failure gives it up here.
if [ -z "$SERVICE_BASE" -o ! -r "$SERVICE_BASE/$YMLFILE" ]; then
echo "$MSG_MISSINGYML" >&2; exit 1
fi
# Sets the absolute paths.
BACKUPDIR="${PAR_BACKUPDIR:-$SERVICE_BASE/$BACKUPDIR}"
# Locates the source folder.
#
# Maybe given as a command line parameter.
[[ -n "$1" ]] && SOURCEDIR="$1" && shift
[[ -z "$SOURCEDIR" ]] && SOURCEDIR="$SERVICE_BASE/storage/volumes/redmine_files"
# Gives up here if doesn't found.
if [ -z "$SOURCEDIR" -o ! -d "$("$READLINK" -e "$SOURCEDIR")" ]; then
echo "$MSG_MISSINGSOURCE $SOURCEDIR"; exit 1
fi
# Locates the backup (git) folder.
#
# Maybe given as a command line parameter.
[[ -n "$1" ]] && GITDIR="$1" && shift
[[ -z "$GITDIR" ]] && GITDIR="$SERVICE_BASE/$GITPATH"
# Gives up here if doesn't found.
if [ -z "$GITDIR" -o ! -d "$("$READLINK" -e "$GITDIR")" ]; then
echo "$MSG_MISSINGGIT $GITDIR"; exit 1
fi
# Does it writable?
( cd "$GITDIR" 2>/dev/null
if [ ! "$PWD" = "$GITDIR" -o ! -w "$PWD" ]; then
echo "$MSG_WRONGGIT $GITDIR"; exit 1
fi
) || exit 1
# We've the folders localized.
# The service must be running - silently gives up here if not.
#
[[ -z "$(cd "$SERVICE_BASE"; "$DOCKER_COMPOSE" $commandstring ps --services --filter "status=running")" ]] \
&& exit 1
# Attempts the backup commit.
#
# Initializes the git backup if it doesn't exist yet.
if [ ! -d "$GITDIR/.git" ]; then
# Initializes the repo itself.
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" init --quiet
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" config user.name "$BOTNAME"
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" config user.email "$BOTEMAIL"
fi
# Stages all the files and non-empty folders.
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" add . >/dev/null
# Stores the file system metadata as well, if the tool has been installed.
if [ ! -z "$(which metastore)" -a -x "$(which metastore)" ]; then
# This commamd silently creates the metastore file if it doesnt' exist yet.
( cd "$SOURCEDIR"
"$(which metastore)" -smqq --file ".metadata"
)
# Stages it as well.
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" add ".metadata" >/dev/null
fi
# Makes the commit.
"$GIT" --git-dir="$GITDIR/.git" --work-tree="$SOURCEDIR" commit --quiet -m "'$MSG_GITCOMMIT $("$DATE" '+%Y%m%d-%H%M%S')'"
# Git done.
# That's all, Folks! :)

View File

@@ -0,0 +1,24 @@
#!/bin/bash
#
# Downloads a copy of the latest versions of the installed plugins
# into a temporary destination folder. Doesn't hurt the running
# instance.
#
# To perform the actual upgrade you have to stop the instance and manually
# upgrade the contents of the storage/volumes/redmine_plugins folder.
# Where I'm?
SCRPATH="$( cd -P "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" #"
# Temporary destination folder for download.
PLUGINS=$(readlink -f "$SCRPATH/../storage/backups/plugins/latest_v6")
if [ ! -d "$PLUGINS" -o ! -w "$PLUGINS" ]; then
echo -e "Bad destination: $PLUGINS" >&2; exit 1
fi
# Plugins from GitHub.
# Feel free to adjust it to your needs.
#rm -rf "$PLUGINS/additionals" 2>/dev/null
#git clone -b stable https://github.com/alphanodes/additionals.git "$PLUGINS/additionals"
# Some cleanup
rm -rf "$PLUGINS"/*/.git "$PLUGINS"/*/.github

View File

@@ -0,0 +1,195 @@
#!/bin/bash
#
# Restores a composed MySQL/MariaDB database from a dump file.
# Gets all necessary data from the docker-compose.yml file.
#
# This is a wrapper script to the system-wide mysql_restoredb tool.
# Database recovey with the necessary user management and grants
# requires superuser privileges in MySQL, but simple data recovery
# is possible if the user and privileges are already set.
#
# You have to call this script as a Docker manager user (member of the
# 'docker' Linux group). The worker tool must be available somewhere
# in PATH. At least 5.7.6 MySQL or at least 10.1.3 MariaDB is required.
#
# Usage:
# $0 path_to_the_dumpfile [ path_to_the_service's_base ]
#
# Author: Kovács Zoltán <kovacsz@marcusconsulting.hu>
# License: GNU/GPL v3+ (https://www.gnu.org/licenses/gpl-3.0.en.html)
#
# 2025-02-26 v0.1 Forked from the Smartfront repository and rewritten.
# Accepted environment variables and their defaults.
#
PAR_SERVICE=${SERVICE:-"database"} # Database container's name
# Other initialisations.
#
BACKUPFOLDER="storage/backups/dumps" # Skeleton's default dump folder
PROP_DBAPASS="MYSQL_ROOT_PASSWORD" # DB admin password property
PROP_DBNAME="MYSQL_DATABASE" # DB name property
PROP_DBPASS="MYSQL_PASSWORD" # DB password property
PROP_DBUSER="MYSQL_USER" # DB username property
USER=${USER:-LOGNAME} # Fix for cron enviroment only
YMLFILE="docker-compose.yml"
# Basic environment settings.
#
LANG=C
LC_ALL=C
# Messages.
#
MSG_BADDUMP="Fatal: doesn't exist or doesn't a dumpfile:"
MSG_DOCKERGRPNEED="You must be a member of the docker group."
MSG_DOESNOTRUN="This service doesn't run."
MSG_MISSINGDEP="Fatal: missing dependency"
MSG_MISSINGCONF="Fatal: missing config file"
MSG_MISSINGYML="Fatal: didn't find the $YMLFILE file"
MSG_NOLOCATE="Cannot locate the database container."
MSG_NOPARAM="Missing environment parameter"
MSG_USAGE="Usage: $0 dump_pathname [ composition_base_pathname ]\n"
MSG_USAGE+="ENVVAR:\n"
MSG_USAGE+="SERVICE \tDatabase service's name in composition\n"
# Checks the dependencies.
#
TR=$(which tr 2>/dev/null)
if [ -z "$TR" ]; then echo "$MSG_MISSINGDEP tr."; exit 1 ; fi
for item in basename cat cut date dirname docker \
grep id mysql_restoredb readlink tail xargs
do
if [ -n "$(which $item)" ]
then export $(echo $item | "$TR" '[:lower:]' '[:upper:]' | "$TR" '-' '_')=$(which $item)
else echo "$MSG_MISSINGDEP $item." >&2; exit 1; fi
done
# All dependencies are available via "$THECOMMAND" (upper case) call.
#
# Let's find which version of docker-compose is installed.
if [ $($DOCKER compose version >/dev/null 2>&1; echo $?) -eq 0 ]; then
# We'll use v2 if it is available.
DOCKER_COMPOSE="$DOCKER"
commandstring="compose"
else
# Otherwise falling back to v1.
DOCKER_COMPOSE="$(which docker-compose)"
commandstring=""
fi
# One of the two is mandatory.
if [ -z "$DOCKER_COMPOSE" ];then echo "$MSG_MISSINGDEP docker-compose" >&2; exit 1; fi
# Below docker-compose should be called as "$DOCKER_COMPOSE" $commandstring sequence.
# Where I'm?
# https://gist.github.com/TheMengzor/968e5ea87e99d9c41782
SOURCE="$0"
while [ -h "$SOURCE" ]; do
# resolve $SOURCE until the file is no longer a symlink
SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
SOURCE="$("$READLINK" "$SOURCE")"
# if $SOURCE was a relative symlink, we need to resolve it
# relative to the path where the symlink file was located
[[ $SOURCE != /* ]] && SOURCE="$SCRPATH/$SOURCE"
done; SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && echo "$PWD" )" #"
# Need to be root or a Docker manager user.
#
[[ "$USER" != 'root' ]] \
&& [[ -z "$(echo "$("$ID" -Gn "$USER") " | "$GREP" ' docker ')" ]] \
&& echo "$MSG_DOCKERGRPNEED" >&2 && exit 1 #"
# Gets the command line parameters.
#
# DUMPFILE is mandatory
if [ -n "$1" ]; then DUMPFILE="$1"; shift
else echo -e "$MSG_USAGE" >&2; exit 1; fi
# SERVICE_BASE is optional
if [ -n "$1" ]; then SERVICE_BASE="$1"; shift; fi
# We've read the unchecked command line parameters.
# Searches the base folder, containing the YMLFILE.
#
if [ -z "$SERVICE_BASE" ]; then
# Called from the base folder (./)?
TEST_DIR="$SCRPATH"
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
# Called from ./tools?
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
# Called from ./tools/*.d?
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
[[ -z "$SERVICE_BASE" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && SERVICE_BASE="$TEST_DIR"
fi
# On failure gives it up here.
if [ -z "$SERVICE_BASE" -o ! -r "$SERVICE_BASE/$YMLFILE" ]; then
echo "$MSG_MISSINGYML" >&2; exit 1
fi
# Sets the absolute path.
YMLFILE="$SERVICE_BASE/$YMLFILE"
# We've the YMLFILE.
# Finds the DUMPFILE to use.
#
# The DUMPFILE must point to a readable file.
# If doesn't it tries the skeleton's standard backup folder as well.
if [ ! -r "$DUMPFILE" ]
then DUMPFILE="$("$DIRNAME" "$SERVICE_BASE")/$BACKUPFOLDER/$DUMPFILE"; fi
# If it is an existing symlink dereferences it to ensure, it points to a file.
if [ -h "$DUMPFILE" ]; then
if [[ "$("$READLINK" "$DUMPFILE")" != /* ]]
# relative path in symlink
then DUMPFILE="$("$DIRNAME" "$DUMPFILE")/$("$READLINK" "$DUMPFILE")"
# absolute path in symlink
else DUMPFILE="$("$READLINK" "$DUMPFILE")"; fi
fi
# Let's check it!
if [ ! -r "$DUMPFILE" -o ! -f "$DUMPFILE" ]
then echo -e "$MSG_BADDUMP $DUMPFILE"; exit 1; fi
# We've an existing dumpfile.
# The composition must be running - silently gives up here if not.
#
[[ -z "$(cd "$SERVICE_BASE"; "$DOCKER_COMPOSE" $commandstring ps --services --filter "status=running")" ]] \
&& exit 1
# Parses the YMLFILE for parameters to use.
#
function parse { [[ -z "$1" ]] && return
# Gets the live lines containing the parameter.
value=$("$CAT" "$YMLFILE" | "$GREP" -ve '^#' | \
"$GREP" -e "^ *$1" | "$TR" -d '\r')
# If multiple the last one to consider.
value=$(echo -e "$value" | "$TAIL" -n1)
# Right side of the colon W/O leading and trailing spaces and quotes.
value=$(echo -ne "$value" | "$CUT" -d':' -f2 | "$XARGS")
# Removes the trailing semicolon (if any).
value=${value%;*}
echo -e "$value"; return
}
# These parameters are mandatory.
MYCONTAINER="$PAR_SERVICE" # TODO: guess from the yml
if [ -z "$MYCONTAINER" ]; then echo "$MSG_NOPARAM PAR_SERVICE" >&2; exit 1; fi1; fi
MYDATABASE="$(parse "$PROP_DBNAME")"
if [ -z "$MYDATABASE" ]; then echo "$MSG_NOPARAM $PROP_DBNAME" >&2; exit 1; fi
MYUSER="$(parse "$PROP_DBUSER")"
if [ -z "$MYUSER" ]; then echo "$MSG_NOPARAM $PROP_DBUSER" >&2; exit 1; fi
MYPASSWORD="$(parse "$PROP_DBPASS")"
if [ -z "$MYPASSWORD" ]; then echo "$MSG_NOPARAM $PROP_DBPASS" >&2; exit 1; fi
# These are optional.
MYDBAUSER="root"
MYDBAPASSWORD="$(parse "$PROP_DBAPASS")"
# We've the configuration parsed.
# Converts the database service name to an actual running container's name.
#
MYCONTAINER="$("$DOCKER" inspect -f '{{.Name}}' $(cd "$SERVICE_BASE"; "$DOCKER_COMPOSE" $commandstring ps -q "$MYCONTAINER") | "$CUT" -c2-)"
# Gives up here if failed.
if [ -z "$MYCONTAINER" ]; then echo "$MSG_NOLOCATE" >&2; exit 1; fi
# Calls the worker script to make the job.
#
export MYDBAUSER MYDBAPASSWORD MYPASSWORD
"$MYSQL_RESTOREDB" -C "$MYCONTAINER" -U "$MYUSER" "$MYDATABASE" "$DUMPFILE"
# That's all, Folks! :)

View File

@@ -1,9 +1,8 @@
# This is a shell script excerpt for configuration purposes only.
# Handle with care! Please don't put code here, only variables.
CLASSES_PATTERN="^([^.]*)\..*\.$HOSTNAME\.(dmp|sql\.gz|tgz|log)$"
CLASSES_PATTERN="^([^.]*)\..*\.$HOSTNAME\.(dmp|sql\.gz|tar|tgz|log)$"
DOIT="yes" # if empty the script makes a dry run
# RETAIN_DAYS=7 # retains all files created within that many days
# RETAIN_WEEKS=4 # retains one file per week/month,
# RETAIN_MONTHS=12 # created within that many weeks/months

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env sh
VER=3.0.8
VER=3.1.3
PROJECT_NAME="acme.sh"
@@ -23,9 +23,6 @@ _SUB_FOLDERS="$_SUB_FOLDER_DNSAPI $_SUB_FOLDER_DEPLOY $_SUB_FOLDER_NOTIFY"
CA_LETSENCRYPT_V2="https://acme-v02.api.letsencrypt.org/directory"
CA_LETSENCRYPT_V2_TEST="https://acme-staging-v02.api.letsencrypt.org/directory"
CA_BUYPASS="https://api.buypass.com/acme/directory"
CA_BUYPASS_TEST="https://api.test4.buypass.no/acme/directory"
CA_ZEROSSL="https://acme.zerossl.com/v2/DV90"
_ZERO_EAB_ENDPOINT="https://api.zerossl.com/acme/eab-credentials-email"
@@ -35,6 +32,8 @@ CA_SSLCOM_ECC="https://acme.ssl.com/sslcom-dv-ecc"
CA_GOOGLE="https://dv.acme-v02.api.pki.goog/directory"
CA_GOOGLE_TEST="https://dv.acme-v02.test-api.pki.goog/directory"
CA_ACTALIS="https://acme-api.actalis.com/acme/directory"
DEFAULT_CA=$CA_ZEROSSL
DEFAULT_STAGING_CA=$CA_LETSENCRYPT_V2_TEST
@@ -42,14 +41,13 @@ CA_NAMES="
ZeroSSL.com,zerossl
LetsEncrypt.org,letsencrypt
LetsEncrypt.org_test,letsencrypt_test,letsencrypttest
BuyPass.com,buypass
BuyPass.com_test,buypass_test,buypasstest
SSL.com,sslcom
Google.com,google
Google.com_test,googletest,google_test
Actalis.com,actalis.com,actalis
"
CA_SERVERS="$CA_ZEROSSL,$CA_LETSENCRYPT_V2,$CA_LETSENCRYPT_V2_TEST,$CA_BUYPASS,$CA_BUYPASS_TEST,$CA_SSLCOM_RSA,$CA_GOOGLE,$CA_GOOGLE_TEST"
CA_SERVERS="$CA_ZEROSSL,$CA_LETSENCRYPT_V2,$CA_LETSENCRYPT_V2_TEST,$CA_SSLCOM_RSA,$CA_GOOGLE,$CA_GOOGLE_TEST,$CA_ACTALIS"
DEFAULT_USER_AGENT="$PROJECT_NAME/$VER ($PROJECT)"
@@ -67,7 +65,7 @@ ID_TYPE_IP="ip"
LOCAL_ANY_ADDRESS="0.0.0.0"
DEFAULT_RENEW=60
DEFAULT_RENEW=30
NO_VALUE="no"
@@ -180,6 +178,8 @@ _VALIDITY_WIKI="https://github.com/acmesh-official/acme.sh/wiki/Validity"
_DNSCHECK_WIKI="https://github.com/acmesh-official/acme.sh/wiki/dnscheck"
_PROFILESELECTION_WIKI="https://github.com/acmesh-official/acme.sh/wiki/Profile-selection"
_DNS_MANUAL_ERR="The dns manual mode can not renew automatically, you must issue it again manually. You'd better use the other modes instead."
_DNS_MANUAL_WARN="It seems that you are using dns manual mode. please take care: $_DNS_MANUAL_ERR"
@@ -250,6 +250,13 @@ _dlg_versions() {
socat -V 2>&1
else
_debug "socat doesn't exist."
if _exists "python3"; then
python3 -V 2>&1
elif _exists "python2"; then
python2 -V 2>&1
elif _exists "python"; then
python -V 2>&1
fi
fi
}
@@ -436,14 +443,28 @@ _secure_debug3() {
fi
}
__USE_TR_TAG=""
if [ "$(echo "abc" | LANG=C tr a-z A-Z 2>/dev/null)" != "ABC" ]; then
__USE_TR_TAG="1"
fi
export __USE_TR_TAG
_upper_case() {
if [ "$__USE_TR_TAG" ]; then
LANG=C tr '[:lower:]' '[:upper:]'
else
# shellcheck disable=SC2018,SC2019
tr '[a-z]' '[A-Z]'
LANG=C tr '[a-z]' '[A-Z]'
fi
}
_lower_case() {
if [ "$__USE_TR_TAG" ]; then
LANG=C tr '[:upper:]' '[:lower:]'
else
# shellcheck disable=SC2018,SC2019
tr '[A-Z]' '[a-z]'
LANG=C tr '[A-Z]' '[a-z]'
fi
}
_startswith() {
@@ -672,8 +693,10 @@ _hex_dump() {
#0 1 2 3 4 5 6 7 8 9 - _ . ~
#30 31 32 33 34 35 36 37 38 39 2d 5f 2e 7e
#_url_encode [upper-hex] the encoded hex will be upper-case if the argument upper-hex is followed
#stdin stdout
_url_encode() {
_upper_hex=$1
_hex_str=$(_hex_dump)
_debug3 "_url_encode"
_debug3 "_hex_str" "$_hex_str"
@@ -883,6 +906,9 @@ _url_encode() {
;;
#other hex
*)
if [ "$_upper_hex" = "upper-hex" ]; then
_hex_code=$(printf "%s" "$_hex_code" | _upper_case)
fi
printf '%%%s' "$_hex_code"
;;
esac
@@ -916,6 +942,9 @@ _sed_i() {
if sed -h 2>&1 | grep "\-i\[SUFFIX]" >/dev/null 2>&1; then
_debug "Using sed -i"
sed -i "$options" "$filename"
elif sed -h 2>&1 | grep "\-i extension" >/dev/null 2>&1; then
_debug "Using FreeBSD sed -i"
sed -i "" "$options" "$filename"
else
_debug "No -i support in sed"
text="$(cat "$filename")"
@@ -1009,7 +1038,7 @@ _digest() {
outputhex="$2"
if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then
if [ "$alg" = "sha3-256" ] || [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then
if [ "$outputhex" ]; then
${ACME_OPENSSL_BIN:-openssl} dgst -"$alg" -hex | cut -d = -f 2 | tr -d ' '
else
@@ -1228,7 +1257,7 @@ _idn() {
fi
}
#_createcsr cn san_list keyfile csrfile conf acmeValidationv1
#_createcsr cn san_list keyfile csrfile conf acmeValidationv1 extendedUsage
_createcsr() {
_debug _createcsr
domain="$1"
@@ -1237,6 +1266,7 @@ _createcsr() {
csr="$4"
csrconf="$5"
acmeValidationv1="$6"
extusage="$7"
_debug2 domain "$domain"
_debug2 domainlist "$domainlist"
_debug2 csrkey "$csrkey"
@@ -1245,9 +1275,8 @@ _createcsr() {
printf "[ req_distinguished_name ]\n[ req ]\ndistinguished_name = req_distinguished_name\nreq_extensions = v3_req\n[ v3_req ]" >"$csrconf"
if [ "$Le_ExtKeyUse" ]; then
_savedomainconf Le_ExtKeyUse "$Le_ExtKeyUse"
printf "\nextendedKeyUsage=$Le_ExtKeyUse\n" >>"$csrconf"
if [ "$extusage" ]; then
printf "\nextendedKeyUsage=$extusage\n" >>"$csrconf"
else
printf "\nextendedKeyUsage=serverAuth,clientAuth\n" >>"$csrconf"
fi
@@ -1393,6 +1422,12 @@ _ss() {
return 0
fi
if [ "$(uname)" = "AIX" ]; then
_debug "Using: AIX netstat"
netstat -an | grep "^tcp" | grep "LISTEN" | grep "\.$_port "
return 0
fi
if _exists "netstat"; then
_debug "Using: netstat"
if netstat -help 2>&1 | grep "\-p proto" >/dev/null; then
@@ -1437,8 +1472,8 @@ _toPkcs() {
else
${ACME_OPENSSL_BIN:-openssl} pkcs12 -export -out "$_cpfx" -inkey "$_ckey" -in "$_ccert" -certfile "$_cca"
fi
if [ "$?" == "0" ]; then
_savedomainconf "Le_PFXPassword" "$pfxPassword"
if [ "$?" = "0" ]; then
_savedomainconf "Le_PFXPassword" "$pfxPassword" "base64"
fi
}
@@ -1623,6 +1658,11 @@ _time2str() {
return
fi
#Omnios
if date -u -r "$1" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null; then
return
fi
#Solaris
if printf "%(%Y-%m-%dT%H:%M:%SZ)T\n" $1 2>/dev/null; then
return
@@ -1792,6 +1832,10 @@ _time() {
# 2022-04-01 08:10:33 to 1648800633
#or 2022-04-01T08:10:33Z to 1648800633
_date2time() {
#Mac/BSD
if date -u -j -f "%Y-%m-%d %H:%M:%S" "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then
return
fi
#Linux
if date -u -d "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then
return
@@ -1801,12 +1845,12 @@ _date2time() {
if gdate -u -d "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then
return
fi
#Mac/BSD
if date -u -j -f "%Y-%m-%d %H:%M:%S" "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then
#Omnios
if python3 -c "import datetime; print(int(datetime.datetime.strptime(\"$1\", \"%Y-%m-%d %H:%M:%S\").replace(tzinfo=datetime.timezone.utc).timestamp()))" 2>/dev/null; then
return
fi
#Omnios
if da="$(echo "$1" | tr -d "Z" | tr "T" ' ')" perl -MTime::Piece -e 'print Time::Piece->strptime($ENV{da}, "%Y-%m-%d %H:%M:%S")->epoch, "\n";' 2>/dev/null; then
if python3 -c "import datetime; print(int(datetime.datetime.strptime(\"$1\", \"%Y-%m-%dT%H:%M:%SZ\").replace(tzinfo=datetime.timezone.utc).timestamp()))" 2>/dev/null; then
return
fi
_err "Cannot parse _date2time $1"
@@ -1860,6 +1904,11 @@ _inithttp() {
if [ -z "$_ACME_CURL" ] && _exists "curl"; then
_ACME_CURL="curl --silent --dump-header $HTTP_HEADER "
if [ "$ACME_USE_IPV6_REQUESTS" ]; then
_ACME_CURL="$_ACME_CURL --ipv6 "
elif [ "$ACME_USE_IPV4_REQUESTS" ]; then
_ACME_CURL="$_ACME_CURL --ipv4 "
fi
if [ -z "$ACME_HTTP_NO_REDIRECTS" ]; then
_ACME_CURL="$_ACME_CURL -L "
fi
@@ -1887,6 +1936,11 @@ _inithttp() {
if [ -z "$_ACME_WGET" ] && _exists "wget"; then
_ACME_WGET="wget -q"
if [ "$ACME_USE_IPV6_REQUESTS" ]; then
_ACME_WGET="$_ACME_WGET --inet6-only "
elif [ "$ACME_USE_IPV4_REQUESTS" ]; then
_ACME_WGET="$_ACME_WGET --inet4-only "
fi
if [ "$ACME_HTTP_NO_REDIRECTS" ]; then
_ACME_WGET="$_ACME_WGET --max-redirect 0 "
fi
@@ -2188,7 +2242,6 @@ _send_signed_request() {
_debug2 _headers "$_headers"
_CACHED_NONCE="$(echo "$_headers" | grep -i "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)"
fi
_debug2 _CACHED_NONCE "$_CACHED_NONCE"
if [ "$?" != "0" ]; then
_err "Cannot connect to $nonceurl to get nonce."
return 1
@@ -2305,6 +2358,7 @@ _setopt() {
fi
if [ ! -f "$__conf" ]; then
touch "$__conf"
chmod 600 "$__conf"
fi
if [ -n "$(_tail_c 1 <"$__conf")" ]; then
echo >>"$__conf"
@@ -2361,7 +2415,7 @@ _clear_conf() {
_sdkey="$2"
if [ "$_c_c_f" ]; then
_conf_data="$(cat "$_c_c_f")"
echo "$_conf_data" | sed "s/^$_sdkey *=.*$//" >"$_c_c_f"
echo "$_conf_data" | sed "/^$_sdkey *=.*$/d" >"$_c_c_f"
else
_err "Config file is empty, cannot clear"
fi
@@ -2513,18 +2567,23 @@ _startserver() {
_debug Le_Listen_V4 "$Le_Listen_V4"
_debug Le_Listen_V6 "$Le_Listen_V6"
if _exists "socat"; then
_NC="socat"
if [ "$Le_Listen_V6" ]; then
_NC="$_NC -6"
else
SOCAT_OPTIONS=TCP6-LISTEN
elif [ "$Le_Listen_V4" ]; then
_NC="$_NC -4"
SOCAT_OPTIONS=TCP4-LISTEN
else
SOCAT_OPTIONS=TCP-LISTEN
fi
if [ "$DEBUG" ] && [ "$DEBUG" -gt "1" ]; then
_NC="$_NC -d -d -v"
fi
SOCAT_OPTIONS=TCP-LISTEN:$Le_HTTPPort,crlf,reuseaddr,fork
SOCAT_OPTIONS=$SOCAT_OPTIONS:$Le_HTTPPort,crlf,reuseaddr,fork
#Adding bind to local-address
if [ "$ncaddr" ]; then
@@ -2541,9 +2600,43 @@ echo 'Content-Length\: $_content_len'; \
echo ''; \
printf '%s' '$content';" 2>"$_SOCAT_ERR" &
serverproc="$!"
else
_PYTHON=""
if _exists "python3"; then
_PYTHON="python3"
elif _exists "python2"; then
_PYTHON="python2"
elif _exists "python"; then
_PYTHON="python"
fi
if [ "$_PYTHON" ]; then
_debug "Using python: $_PYTHON"
_AF="socket.AF_INET"
_BIND_ADDR="0.0.0.0"
if [ "$Le_Listen_V6" ]; then
_AF="socket.AF_INET6"
_BIND_ADDR="::"
fi
if [ "$ncaddr" ]; then
_BIND_ADDR="$ncaddr"
fi
export _SOCAT_ERR="$(_mktemp)"
$_PYTHON -c "import socket,sys;s=socket.socket($_AF,socket.SOCK_STREAM);s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1);s.bind((sys.argv[2],int(sys.argv[1])));s.listen(5);res='HTTP/1.0 200 OK\r\nContent-Length: '+str(len(sys.argv[3]))+'\r\n\r\n'+sys.argv[3];
while True:
c,a=s.accept()
c.sendall(res.encode() if hasattr(res, 'encode') else res)
c.close()" "$Le_HTTPPort" "$_BIND_ADDR" "$content" 2>"$_SOCAT_ERR" &
serverproc="$!"
_NC="$_PYTHON"
else
_err "Please install socat or python first for standalone mode."
return 1
fi
fi
if [ -f "$_SOCAT_ERR" ]; then
if grep "Permission denied" "$_SOCAT_ERR" >/dev/null; then
_err "socat: $(cat $_SOCAT_ERR)"
_err "$_NC: $(cat $_SOCAT_ERR)"
_err "Can not listen for user: $(whoami)"
_err "Maybe try with root again?"
rm -f "$_SOCAT_ERR"
@@ -2733,6 +2826,7 @@ _clearAPI() {
ACME_REVOKE_CERT=""
ACME_NEW_NONCE=""
ACME_AGREEMENT=""
ACME_RENEWAL_INFO=""
}
#server
@@ -2745,7 +2839,7 @@ _initAPI() {
_request_retry_times=0
while [ -z "$ACME_NEW_ACCOUNT" ] && [ "${_request_retry_times}" -lt "$MAX_API_RETRY_TIMES" ]; do
_request_retry_times=$(_math "$_request_retry_times" + 1)
response=$(_get "$_api_server")
response=$(_get "$_api_server" "" 10)
if [ "$?" != "0" ]; then
_debug2 "response" "$response"
_info "Cannot init API for: $_api_server."
@@ -2777,6 +2871,9 @@ _initAPI() {
ACME_AGREEMENT=$(echo "$response" | _egrep_o 'termsOfService" *: *"[^"]*"' | cut -d '"' -f 3)
export ACME_AGREEMENT
ACME_RENEWAL_INFO=$(echo "$response" | _egrep_o 'renewalInfo" *: *"[^"]*"' | cut -d '"' -f 3)
export ACME_RENEWAL_INFO
_debug "ACME_KEY_CHANGE" "$ACME_KEY_CHANGE"
_debug "ACME_NEW_AUTHZ" "$ACME_NEW_AUTHZ"
_debug "ACME_NEW_ORDER" "$ACME_NEW_ORDER"
@@ -2784,6 +2881,7 @@ _initAPI() {
_debug "ACME_REVOKE_CERT" "$ACME_REVOKE_CERT"
_debug "ACME_AGREEMENT" "$ACME_AGREEMENT"
_debug "ACME_NEW_NONCE" "$ACME_NEW_NONCE"
_debug "ACME_RENEWAL_INFO" "$ACME_RENEWAL_INFO"
if [ "$ACME_NEW_ACCOUNT" ] && [ "$ACME_NEW_ORDER" ]; then
return 0
fi
@@ -3491,7 +3589,7 @@ _on_before_issue() {
_debug _chk_alt_domains "$_chk_alt_domains"
#run pre hook
if [ "$_chk_pre_hook" ]; then
_info "Runing pre hook:'$_chk_pre_hook'"
_info "Running pre hook:'$_chk_pre_hook'"
if ! (
export Le_Domain="$_chk_main_domain"
export Le_Alt="$_chk_alt_domains"
@@ -3502,9 +3600,9 @@ _on_before_issue() {
fi
fi
if _hasfield "$_chk_web_roots" "$NO_VALUE"; then
if ! _exists "socat"; then
_err "Please install socat tools first."
if _hasfield "$_chk_web_roots" "$NO_VALUE" && [ "$_chk_web_roots" = "$NO_VALUE" ]; then
if ! _exists "socat" && ! _exists "python" && ! _exists "python2" && ! _exists "python3"; then
_err "Please install socat or python tools first."
return 1
fi
fi
@@ -4394,6 +4492,8 @@ issue() {
_preferred_chain="${15}"
_valid_from="${16}"
_valid_to="${17}"
_certificate_profile="${18}"
_extended_key_usage="${19}"
if [ -z "$_ACME_IS_RENEW" ]; then
_initpath "$_main_domain" "$_key_length"
@@ -4413,7 +4513,7 @@ issue() {
Le_NextRenewTime=$(_readdomainconf Le_NextRenewTime)
_debug Le_NextRenewTime "$Le_NextRenewTime"
if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then
_valid_to_saved=$(_readdomainconf Le_Valid_to)
_valid_to_saved=$(_readdomainconf Le_Valid_To)
if [ "$_valid_to_saved" ] && ! _startswith "$_valid_to_saved" "+"; then
_info "The domain is set to be valid to: $_valid_to_saved"
_info "It cannot be renewed automatically"
@@ -4469,6 +4569,11 @@ issue() {
else
_cleardomainconf "Le_Preferred_Chain"
fi
if [ "$_certificate_profile" ]; then
_savedomainconf "Le_Certificate_Profile" "$_certificate_profile"
else
_cleardomainconf "Le_Certificate_Profile"
fi
Le_API="$ACME_DIRECTORY"
_savedomainconf "Le_API" "$Le_API"
@@ -4480,6 +4585,7 @@ issue() {
if ! _on_before_issue "$_web_roots" "$_main_domain" "$_alt_domains" "$_pre_hook" "$_local_addr"; then
_err "_on_before_issue."
_on_issue_err "$_post_hook"
return 1
fi
@@ -4532,12 +4638,25 @@ issue() {
return 1
fi
fi
if ! _createcsr "$_main_domain" "$_alt_domains" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF"; then
_keyusage="$_extended_key_usage"
if [ "$Le_API" = "$CA_GOOGLE" ] || [ "$Le_API" = "$CA_GOOGLE_TEST" ]; then
if [ -z "$_keyusage" ]; then
#https://github.com/acmesh-official/acme.sh/issues/6610
#google accepts serverauth only
_keyusage="serverAuth"
fi
fi
if ! _createcsr "$_main_domain" "$_alt_domains" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF" "" "$_keyusage"; then
_err "Error creating CSR."
_clearup
_on_issue_err "$_post_hook"
return 1
fi
if [ "$_extended_key_usage" ]; then
_savedomainconf "Le_ExtKeyUse" "$_extended_key_usage"
else
_cleardomainconf "Le_ExtKeyUse"
fi
fi
_savedomainconf "Le_Keylength" "$_key_length"
@@ -4600,6 +4719,9 @@ issue() {
if [ "$_notAfter" ]; then
_newOrderObj="$_newOrderObj,\"notAfter\": \"$_notAfter\""
fi
if [ "$_certificate_profile" ]; then
_newOrderObj="$_newOrderObj,\"profile\": \"$_certificate_profile\""
fi
_debug "STEP 1, Ordering a Certificate"
if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then
_err "Error creating new order."
@@ -4739,7 +4861,8 @@ $_authorizations_map"
_debug keyauthorization "$keyauthorization"
fi
entry="$(echo "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')"
# Fix for empty error objects in response which mess up the original code, adapted from fix suggested here: https://github.com/acmesh-official/acme.sh/issues/4933#issuecomment-1870499018
entry="$(echo "$response" | sed s/'"error":{}'/'"error":null'/ | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')"
_debug entry "$entry"
if [ -z "$keyauthorization" -a -z "$entry" ]; then
@@ -4989,9 +5112,11 @@ $_authorizations_map"
_debug "Writing token: $token to $wellknown_path/$token"
mkdir -p "$wellknown_path"
if ! printf "%s" "$keyauthorization" >"$wellknown_path/$token"; then
# Ensure .well-known is visible to web server user/group
# https://github.com/Neilpang/acme.sh/pull/32
if ! (umask ugo+rx &&
mkdir -p "$wellknown_path" &&
printf "%s" "$keyauthorization" >"$wellknown_path/$token"); then
_err "$d: Cannot write token to file: $wellknown_path/$token"
_clearupwebbroot "$_currentRoot" "$removelevel" "$token"
_clearup
@@ -5111,6 +5236,19 @@ $_authorizations_map"
_on_issue_err "$_post_hook" "$vlist"
return 1
fi
_retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *: *[0-9]\+ *" | cut -d : -f 2 | tr -d ' ' | tr -d '\r')
_sleep_overload_retry_sec=$_retryafter
if [ "$_sleep_overload_retry_sec" ]; then
if [ $_sleep_overload_retry_sec -le 600 ]; then
_sleep $_sleep_overload_retry_sec
else
_info "The retryafter=$_retryafter value is too large (> 600), will not retry anymore."
_clearupwebbroot "$_currentRoot" "$removelevel" "$token"
_clearup
_on_issue_err "$_post_hook" "$vlist"
return 1
fi
fi
done
done
@@ -5152,6 +5290,16 @@ $_authorizations_map"
return 1
fi
break
elif _contains "$response" "\"ready\""; then
_info "Order status is 'ready', let's sleep and retry."
_retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r')
_debug "_retryafter" "$_retryafter"
if [ "$_retryafter" ]; then
_info "Sleeping for $_retryafter seconds then retrying"
_sleep $_retryafter
else
_sleep 2
fi
elif _contains "$response" "\"processing\""; then
_info "Order status is 'processing', let's sleep and retry."
_retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r')
@@ -5350,10 +5498,10 @@ $_authorizations_map"
_savedomainconf "Le_NextRenewTime" "$Le_NextRenewTime"
#convert to pkcs12
Le_PFXPassword="$(_readdomainconf Le_PFXPassword)"
if [ "$Le_PFXPassword" ]; then
_toPkcs "$CERT_PFX_PATH" "$CERT_KEY_PATH" "$CERT_PATH" "$CA_CERT_PATH" "$Le_PFXPassword"
fi
export CERT_PFX_PATH
if [ "$_real_cert$_real_key$_real_ca$_reload_cmd$_real_fullchain" ]; then
_savedomainconf "Le_RealCertPath" "$_real_cert"
@@ -5421,10 +5569,6 @@ renew() {
_info "Switching back to $CA_LETSENCRYPT_V2"
Le_API="$CA_LETSENCRYPT_V2"
;;
"$CA_BUYPASS_TEST")
_info "Switching back to $CA_BUYPASS"
Le_API="$CA_BUYPASS"
;;
"$CA_GOOGLE_TEST")
_info "Switching back to $CA_GOOGLE"
Le_API="$CA_GOOGLE"
@@ -5466,6 +5610,11 @@ renew() {
Le_PostHook="$(_readdomainconf Le_PostHook)"
Le_RenewHook="$(_readdomainconf Le_RenewHook)"
Le_Preferred_Chain="$(_readdomainconf Le_Preferred_Chain)"
Le_Certificate_Profile="$(_readdomainconf Le_Certificate_Profile)"
Le_Valid_From="$(_readdomainconf Le_Valid_From)"
Le_Valid_To="$(_readdomainconf Le_Valid_To)"
Le_ExtKeyUse="$(_readdomainconf Le_ExtKeyUse)"
# When renewing from an old version, the empty Le_Keylength means 2048.
# Note, do not use DEFAULT_DOMAIN_KEY_LENGTH as that value may change over
# time but an empty value implies 2048 specifically.
@@ -5473,7 +5622,14 @@ renew() {
if [ -z "$Le_Keylength" ]; then
Le_Keylength=2048
fi
issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" "$Le_Preferred_Chain" "$Le_Valid_From" "$Le_Valid_To"
if [ "$CA_LETSENCRYPT_V2" = "$Le_API" ]; then
#letsencrypt doesn't support ocsp anymore
if [ "$Le_OCSP_Staple" ]; then
export Le_OCSP_Staple=""
_cleardomainconf Le_OCSP_Staple
fi
fi
issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" "$Le_Preferred_Chain" "$Le_Valid_From" "$Le_Valid_To" "$Le_Certificate_Profile" "$Le_ExtKeyUse"
res="$?"
if [ "$res" != "0" ]; then
return "$res"
@@ -5640,6 +5796,10 @@ signcsr() {
_local_addr="${11}"
_challenge_alias="${12}"
_preferred_chain="${13}"
_valid_f="${14}"
_valid_t="${15}"
_cert_prof="${16}"
_en_key_usage="${17}"
_csrsubj=$(_readSubjectFromCSR "$_csrfile")
if [ "$?" != "0" ]; then
@@ -5683,7 +5843,7 @@ signcsr() {
_info "Copying CSR to: $CSR_PATH"
cp "$_csrfile" "$CSR_PATH"
issue "$_csrW" "$_csrsubj" "$_csrdomainlist" "$_csrkeylength" "$_real_cert" "$_real_key" "$_real_ca" "$_reload_cmd" "$_real_fullchain" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_addr" "$_challenge_alias" "$_preferred_chain"
issue "$_csrW" "$_csrsubj" "$_csrdomainlist" "$_csrkeylength" "$_real_cert" "$_real_key" "$_real_ca" "$_reload_cmd" "$_real_fullchain" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_addr" "$_challenge_alias" "$_preferred_chain" "$_valid_f" "$_valid_t" "$_cert_prof" "$_en_key_usage"
}
@@ -5734,9 +5894,10 @@ list() {
_sep="|"
if [ "$_raw" ]; then
if [ -z "$_domain" ]; then
printf "%s\n" "Main_Domain${_sep}KeyLength${_sep}SAN_Domains${_sep}CA${_sep}Created${_sep}Renew"
printf "%s\n" "Main_Domain${_sep}KeyLength${_sep}SAN_Domains${_sep}Profile${_sep}CA${_sep}Created${_sep}Renew"
fi
for di in "${CERT_HOME}"/*.*/; do
for di in "${CERT_HOME}"/*.* "${CERT_HOME}"/*:*; do
[ -d "$di" ] || continue
d=$(basename "$di")
_debug d "$d"
(
@@ -5749,7 +5910,7 @@ list() {
. "$DOMAIN_CONF"
_ca="$(_getCAShortName "$Le_API")"
if [ -z "$_domain" ]; then
printf "%s\n" "$Le_Domain${_sep}\"$Le_Keylength\"${_sep}$Le_Alt${_sep}$_ca${_sep}$Le_CertCreateTimeStr${_sep}$Le_NextRenewTimeStr"
printf "%s\n" "$Le_Domain${_sep}\"$Le_Keylength\"${_sep}$Le_Alt${_sep}$Le_Certificate_Profile${_sep}$_ca${_sep}$Le_CertCreateTimeStr${_sep}$Le_NextRenewTimeStr"
else
if [ "$_domain" = "$d" ]; then
cat "$DOMAIN_CONF"
@@ -5768,6 +5929,48 @@ list() {
}
list_profiles() {
_initpath
_initAPI
_l_server_url="$ACME_DIRECTORY"
_l_server_name="$(_getCAShortName "$_l_server_url")"
_info "Fetching profiles from $_l_server_name ($_l_server_url)..."
response=$(_get "$_l_server_url" "" 10)
if [ "$?" != "0" ]; then
_err "Failed to connect to CA directory: $_l_server_url"
return 1
fi
normalized_response=$(echo "$response" | _normalizeJson)
profiles_json=$(echo "$normalized_response" | _egrep_o '"profiles" *: *\{[^\}]*\}')
if [ -z "$profiles_json" ]; then
_info "The CA '$_l_server_name' does not publish certificate profiles via its directory endpoint."
return 0
fi
# Strip the outer layer to get the key-value pairs
profiles_kv=$(echo "$profiles_json" | sed 's/"profiles" *: *{//' | sed 's/}$//' | tr ',' '\n')
printf "\n%-15s %s\n" "name" "info"
printf -- "--------------------------------------------------------------------\n"
_old_IFS="$IFS"
IFS='
'
for pair in $profiles_kv; do
# Trim quotes and whitespace
_name=$(echo "$pair" | cut -d: -f1 | tr -d '" \t')
_info_url=$(echo "$pair" | cut -d: -f2- | sed 's/^ *//' | tr -d '"')
printf "%-15s %s\n" "$_name" "$_info_url"
done
IFS="$_old_IFS"
return 0
}
_deploy() {
_d="$1"
_hooks="$2"
@@ -5792,7 +5995,7 @@ _deploy() {
return 1
fi
if ! $d_command "$_d" "$CERT_KEY_PATH" "$CERT_PATH" "$CA_CERT_PATH" "$CERT_FULLCHAIN_PATH"; then
if ! $d_command "$_d" "$CERT_KEY_PATH" "$CERT_PATH" "$CA_CERT_PATH" "$CERT_FULLCHAIN_PATH" "$CERT_PFX_PATH"; then
_err "Error deploying for domain: $_d"
return 1
fi
@@ -5955,7 +6158,7 @@ _installcert() {
); then
_info "$(__green "Reload successful")"
else
_err "Reload error for: $Le_Domain"
_err "Reload error for: $_main_domain"
fi
fi
@@ -6035,7 +6238,7 @@ installcronjob() {
_script="$(_readlink "$_SCRIPT_")"
_debug _script "$_script"
if [ -f "$_script" ]; then
_info "Usinging the current script from: $_script"
_info "Using the current script from: $_script"
lesh="$_script"
else
_err "Cannot install cronjob, $PROJECT_ENTRY not found."
@@ -6306,7 +6509,8 @@ _deactivate() {
fi
_debug "Trigger validation."
vtype="$(_getIdType "$_d_domain")"
entry="$(echo "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')"
# Fix for empty error objects in response which mess up the original code, adapted from fix suggested here: https://github.com/acmesh-official/acme.sh/issues/4933#issuecomment-1870499018
entry="$(echo "$response" | sed s/'"error":{}'/'"error":null'/ | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')"
_debug entry "$entry"
if [ -z "$entry" ]; then
_err "$d: Cannot get domain token"
@@ -6390,6 +6594,36 @@ deactivate() {
done
}
#cert
_getAKI() {
_cert="$1"
openssl x509 -in "$_cert" -text -noout | grep "X509v3 Authority Key Identifier" -A 1 | _tail_n 1 | tr -d ' :'
}
#cert
_getSerial() {
_cert="$1"
openssl x509 -in "$_cert" -serial -noout | cut -d = -f 2
}
#cert
_get_ARI() {
_cert="$1"
_aki=$(_getAKI "$_cert")
_ser=$(_getSerial "$_cert")
_debug2 "_aki" "$_aki"
_debug2 "_ser" "$_ser"
_akiurl="$(echo "$_aki" | _h2b | _base64 | tr -d = | _url_encode)"
_debug2 "_akiurl" "$_akiurl"
_serurl="$(echo "$_ser" | _h2b | _base64 | tr -d = | _url_encode)"
_debug2 "_serurl" "$_serurl"
_ARI_URL="$ACME_RENEWAL_INFO/$_akiurl.$_serurl"
_get "$_ARI_URL"
}
# Detect profile file if not specified as environment variable
_detect_profile() {
if [ -n "$PROFILE" -a -f "$PROFILE" ]; then
@@ -6438,6 +6672,7 @@ _initconf() {
#NO_TIMESTAMP=1
" >"$ACCOUNT_CONF_PATH"
chmod 600 "$ACCOUNT_CONF_PATH"
fi
}
@@ -6473,9 +6708,9 @@ _precheck() {
return 1
fi
if ! _exists "socat"; then
_err "It is recommended to install socat first."
_err "We use socat for the standalone server, which is used for standalone mode."
if ! _exists "socat" && ! _exists "python" && ! _exists "python2" && ! _exists "python3"; then
_err "It is recommended to install socat or python first."
_err "We use socat or python for the standalone server, which is used for standalone mode."
_err "If you don't want to use standalone mode, you may ignore this warning."
fi
@@ -6787,7 +7022,7 @@ _send_notify() {
_nsource="$NOTIFY_SOURCE"
if [ -z "$_nsource" ]; then
_nsource="$(hostname)"
_nsource="$(uname -n)"
fi
_nsubject="$_nsubject by $_nsource"
@@ -6945,6 +7180,9 @@ Parameters:
If no match, the default offered chain will be used. (default: empty)
See: $_PREFERRED_CHAIN_WIKI
--cert-profile, --certificate-profile <profile> If the CA offers profiles, select the desired profile
See: $_PROFILESELECTION_WIKI
--valid-to <date-time> Request the NotAfter field of the cert.
See: $_VALIDITY_WIKI
--valid-from <date-time> Request the NotBefore field of the cert.
@@ -6989,7 +7227,7 @@ Parameters:
--accountconf <file> Specifies a customized account config file.
--home <directory> Specifies the home dir for $PROJECT_NAME.
--cert-home <directory> Specifies the home dir to save all the certs, only valid for '--install' command.
--cert-home <directory> Specifies the home dir to save all the certs.
--config-home <directory> Specifies the home dir to save all the configurations.
--useragent <string> Specifies the user agent string. it will be saved for future use too.
-m, --email <email> Specifies the account email, only valid for the '--install' and '--update-account' command.
@@ -7021,6 +7259,8 @@ Parameters:
--auto-upgrade [0|1] Valid for '--upgrade' command, indicating whether to upgrade automatically in future. Defaults to 1 if argument is omitted.
--listen-v4 Force standalone/tls server to listen at ipv4.
--listen-v6 Force standalone/tls server to listen at ipv6.
--request-v4 Force client requests to use ipv4 to connect to the CA server.
--request-v6 Force client requests to use ipv6 to connect to the CA server.
--openssl-bin <file> Specifies a custom openssl bin location.
--use-wget Force to use wget, if you have both curl and wget installed.
--yes-I-know-dns-manual-mode-enough-go-ahead-please Force use of dns manual mode.
@@ -7139,6 +7379,24 @@ _processAccountConf() {
_saveaccountconf "ACME_USE_WGET" "$ACME_USE_WGET"
fi
if [ "$_request_v6" ]; then
_saveaccountconf "ACME_USE_IPV6_REQUESTS" "$_request_v6"
_clearaccountconf "ACME_USE_IPV4_REQUESTS"
ACME_USE_IPV4_REQUESTS=
elif [ "$_request_v4" ]; then
_saveaccountconf "ACME_USE_IPV4_REQUESTS" "$_request_v4"
_clearaccountconf "ACME_USE_IPV6_REQUESTS"
ACME_USE_IPV6_REQUESTS=
elif [ "$ACME_USE_IPV6_REQUESTS" ]; then
_saveaccountconf "ACME_USE_IPV6_REQUESTS" "$ACME_USE_IPV6_REQUESTS"
_clearaccountconf "ACME_USE_IPV4_REQUESTS"
ACME_USE_IPV4_REQUESTS=
elif [ "$ACME_USE_IPV4_REQUESTS" ]; then
_saveaccountconf "ACME_USE_IPV4_REQUESTS" "$ACME_USE_IPV4_REQUESTS"
_clearaccountconf "ACME_USE_IPV6_REQUESTS"
ACME_USE_IPV6_REQUESTS=
fi
}
_checkSudo() {
@@ -7304,6 +7562,8 @@ _process() {
_local_address=""
_log_level=""
_auto_upgrade=""
_request_v4=""
_request_v6=""
_listen_v4=""
_listen_v6=""
_openssl_bin=""
@@ -7320,6 +7580,8 @@ _process() {
_preferred_chain=""
_valid_from=""
_valid_to=""
_certificate_profile=""
_extended_key_usage=""
while [ ${#} -gt 0 ]; do
case "${1}" in
@@ -7423,6 +7685,9 @@ _process() {
--set-default-chain)
_CMD="setdefaultchain"
;;
--list-profiles)
_CMD="list_profiles"
;;
-d | --domain)
_dvalue="$2"
@@ -7638,6 +7903,10 @@ _process() {
_valid_to="$2"
shift
;;
--certificate-profile | --cert-profile)
_certificate_profile="$2"
shift
;;
--httpport)
_httpport="$2"
Le_HTTPPort="$_httpport"
@@ -7708,7 +7977,7 @@ _process() {
shift
;;
--extended-key-usage)
Le_ExtKeyUse="$2"
_extended_key_usage="$2"
shift
;;
--ocsp-must-staple | --ocsp)
@@ -7761,6 +8030,18 @@ _process() {
fi
AUTO_UPGRADE="$_auto_upgrade"
;;
--request-v4)
_request_v4="1"
ACME_USE_IPV4_REQUESTS="1"
_request_v6=""
ACME_USE_IPV6_REQUESTS=""
;;
--request-v6)
_request_v6="1"
ACME_USE_IPV6_REQUESTS="1"
_request_v4=""
ACME_USE_IPV4_REQUESTS=""
;;
--listen-v4)
_listen_v4="1"
Le_Listen_V4="$_listen_v4"
@@ -7913,13 +8194,13 @@ _process() {
uninstall) uninstall "$_nocron" ;;
upgrade) upgrade ;;
issue)
issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to"
issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to" "$_certificate_profile" "$_extended_key_usage"
;;
deploy)
deploy "$_domain" "$_deploy_hook" "$_ecc"
;;
signcsr)
signcsr "$_csr" "$_webroot" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain"
signcsr "$_csr" "$_webroot" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to" "$_certificate_profile" "$_extended_key_usage"
;;
showcsr)
showcsr "$_csr" "$_domain"
@@ -7984,6 +8265,9 @@ _process() {
setdefaultchain)
setdefaultchain "$_preferred_chain"
;;
list_profiles)
list_profiles
;;
*)
if [ "$_CMD" ]; then
_err "Invalid command: $_CMD"

955
.templates/bin/ssl-cert-check Executable file
View File

@@ -0,0 +1,955 @@
#!/usr/bin/env bash
PROGRAMVERSION=4.14
#
# Program: SSL Certificate Check <ssl-cert-check>
#
# Source code home: https://github.com/Matty9191/ssl-cert-check
#
# Documentation: http://prefetch.net/articles/checkcertificate.html
#
# Author: Matty < matty at prefetch dot net >
#
# Last Updated: 11-12-2020
#
# Revision History:
#
# Version 4.14
# - Fixed HOST / PORT discovery @mhow2
#
# Version 4.13
# - Reverted the file checking logic which breaks $RETCODE
#
# Version 4.12
# - Fixed various logic errors and typos -- Daniel Lewart
#
# Version 4.10
# - Replace tabs with spaces
# - More shllcheck cleanup work
# - Remove unused DEBUG variable
# - Fixed an innocuous whitespace bug in TLSFLAG variable creation
# - Set the default TLS version to 1.1 (can be overridden with -v)
# - Switched openssl CLI options to use an array. The reasons why
# are documented here: http://mywiki.wooledge.org/BashFAQ/050
#
# Version 4.9
# - Add a signal handler to call the cleanup funtion
# if the script doesn't exit() cleanly -- Timothe Litt
#
# Version 4.8
# - More mail client fixes
#
# Version 4.7
# - Revert SENDER to ""
# - More shellcheck cleanup
#
# Version 4.6
# - Fixed programming logic error
#
# Version 4.5
# - Re-work mailx support for FreeBSD
# - More shellcheck fixes
#
# Version 4.4
# - Use command -v instead of which utility to satisfy shellcheck.
# - Fix unquoted MAIL and MAILMODE variables in help output
# - More shellcheck fixes
#
# Version 4.3
# - Fixed a typo in the program version
#
# Version 4.2
# - Change CERTDAYS to CERTDIFF in the e-mail subject.
#
# Version 4.1
# - Fix usage output
#
# Version 4.0
# - Updated the script syntax to align with UNIX shell programming
# - Check for DNS resolution failures
# - First round of updates to make shellcheck happy
# - Rework the logic to call mailx.
# - Print the version with the "-V" option.
# - Define the version in the PROGRAMVERSION variable
#
# Version 3.31
# - Fixed the test for the -servername flag -- Kitson Consulting.
#
# Version 3.30
# - Use highest returncode for Nagios output -- Marcel Pennewiss
# - Set RETCODE to 3 (unknown) if a certificate file does not exist -- Marcel Pennewiss
# - Add a "-d" option to specify a directory or file mask pattern -- Marcel Pennewiss
# - Add a "-N" option to create summarized Nagios output -- Marcel Pennewiss
# - Cleaned up many formatting -- Marcel Pennewiss
#
# Versione 3.29a
# - Added option to specify email sender address
#
# Version 3.29
# - Add the openssl -servername flag if it shows up in help.
#
# Version 3.28
# - Added a DEBUG option to assist with debugging folks who use the script
#
# Version 3.27
# - Allow white spaces to exist in the certificate file list
# - Add an additional check to pick up bad / non-existent certificates
# - Add a check to look for the existence of a mail program. Error out if it's not present.
# - Enable the TLS -servername extension by default - Juergen Knaack & Johan Denoyer
#
# Version 3.26
# - Allow the certificate type (PEM, DER, NET) to be passed on the command line
#
# Version 3.25
# - Check for "no route to host" errors -- Dan Doyle
# - Set RETCODE to 3 (unknown) if a connection error occurs -- Dan Doyle
# - Documentation fixes
#
# Version 3.24
# - Utilize the -clcerts option to limit the results to client certificates - Eitan Katznelson
#
# Version 3.23
# - Fixed typo in date2julian routine -- Ken Cook
#
# Version 3.22
# - Change the validation option to "-V"
# - Add a "-v" option to specify a specific protocol version (ssl2, ssl3 or tls)
#
# Version 3.21
# - Adjust e-mail checking to avoid exiting if notifications aren't enabled -- Nick Anderson
# - Added the number of days until expiration to the Nagios output -- Nick Anderson
#
# Version 3.20
# - Fixed a bug in certificate length checking -- Tim Nowaczyk
#
# Version 3.19
# - Added check to verify the certificate retrieved is valid
#
# Version 3.18
# - Add support for connecting to FTP servers -- Paul A Sand
#
# Version 3.17
# - Add support for connecting to imap servers -- Joerg Pareigis
#
# Version 3.16
# - Add support for connecting to the mail sbmission port -- Luis E. Munoz
#
# Version 3.15
# - Adjusted the file checking logic to use the correct certificate -- Maciej Szudejko
# - Add sbin to the default search paths for OpenBSD compatibility -- Alex Popov
# - Use cut instead of substring processing to ensure compatibility -- Alex Popov
#
# Version 3.14
# - Fixed the Common Name parser to handle DN's where the CN is not the last item
# eg. EmailAddr -- Jason Brothers
# - Added the ability to grab the serial number -- Jason Brothers
# - Added the "-b" option to print results without a header -- Jason Brothers
# - Added the "-v" option for certificate validation -- Jason Brothers
#
# Version 3.13
# - Updated the subject line to include the hostname as well as
# the common name embedded in the X509 certificate (if it's
# available) -- idea proposed by Mike Burns
#
# Version 3.12
# - Updated the license to allow redistribution and modification
#
# Version 3.11
# - Added ability to comment out lines in files passed
# to the "-f" option -- Brett Stauner
# - Fixed comment next to file processing logic
#
# Version 3.10
# - Fixed POP3 port -- Simon Matter
#
# Version 3.9
# - Switched binary location logic to use which utility
#
# Version 3.8
# - Fixed display on 80 column displays
# - Cleaned up the formatting
#
# Version 3.7
# - Fixed bug in NAGIOS tests -- Ben Allen
#
# Version 3.6
# - Added support for certificates stored in PKCS#12 databases -- Ken Gallo
# - Cleaned up comments
# - Adjusted variables to be more consistent
#
# Version 3.5
# - Added support for NAGIOS -- Quanah Gibson-Mount
# - Added additional checks for mail -- Quanah Gibson-Mount
# - Convert tabs to spaces -- Quanah Gibson-Mount
# - Cleaned up usage() routine
# - Added additional checks for openssl
#
# Version 3.4
# - Added a missing "{" to line 364 -- Ken Gallo
# - Move mktemp to the start of the main body to avoid errors
# - Adjusted default binary paths to make sure the script just works
# w/ Solaris, BSD and Linux hosts
#
# Version 3.3
# - Added common name from X.509 certificate file to E-mail body / header -- Doug Curtis
# - Fixed several documentation errors
# - Use mktemp to create temporary files
# - Convert printf, sed and awk to variables
# - Check for printf, sed, awk and mktemp binaries
# - Add additional logic to make sure mktemp returned a valid temporary file
#
# Version 3.2
# - Added option to list certificates in the file passed to "-f".
#
# Version 3.1
# - Added handling for starttls for smtp -- Marco Amrein
# - Added handling for starttls for pop3 (without s) -- Marco Amrein
# - Removed extra spacing at end of script
#
# Version 3.0
# - Added "-i" option to print certificate issuer
# - Removed $0 from Subject line of outbound e-mails
# - Fixed some typographical errors
# - Removed redundant "-b" option
#
# Version 2.0
# - Fixed an issue with e-mails formatting incorrectly
# - Added additional space to host column -- Darren-Perot Spruell
# - Replaced GNU date dependency with CHRIS F. A. JOHNSON's
# date2julian shell function. This routine can be found on
# page 170 of Chris's book "Shell Scripting Recipes: A
# Problem-Solution Approach," ISBN #1590594711. Julian function
# was created based on a post to comp.unix.shell by Tapani Tarvainen.
# - Cleaned up function descriptions
# - Removed several lines of redundant code
# - Adjusted the help message
#
# Version 1.1
# - Added "-c" flag to report expiration status of a PEM encoded
# certificate -- Hampus Lundqvist
# - Updated the prints messages to display the reason a connection
# failed (connection refused, connection timeout, bad cert, etc)
# - Updated the GNU date checking routines
# - Added checks for each binary required
# - Added checks for connection timeouts
# - Added checks for GNU date
# - Added a "-h" option
# - Cleaned up the documentation
#
# Version 1.0
# Initial Release
#
# Purpose:
# ssl-cert-check checks to see if a digital certificate in X.509 format
# has expired. ssl-cert-check can be run in interactive and batch mode,
# and provides facilities to alarm if a certificate is about to expire.
#
# License:
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# Requirements:
# Requires openssl
#
# Installation:
# Copy the shell script to a suitable location
#
# Tested platforms:
# -- Solaris 9 using /bin/bash
# -- Solaris 10 using /bin/bash
# -- OS X 10.4.2 using /bin/bash
# -- OpenBSD using /bin/sh
# -- FreeBSD using /bin/sh
# -- Centos Linux 3, 4, 5 & 6 using /bin/bash
# -- Redhat Enterprise Linux 3, 4, 5 & 6 using /bin/bash
# -- Gentoo using /bin/bash
#
# Usage:
# Refer to the usage() sub-routine, or invoke ssl-cert-check
# with the "-h" option.
#
# Examples:
# Please refer to the following site for documentation and examples:
# http://prefetch.net/articles/checkcertificate.html
# Cleanup temp files if they exist
trap cleanup EXIT INT TERM QUIT
PATH=/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/ssl/bin:/usr/sfw/bin
export PATH
# Who to page when an expired certificate is detected (cmdline: -e)
ADMIN="root"
# Email sender address for alarm notifications
SENDER=""
# Number of days in the warning threshhold (cmdline: -x)
WARNDAYS=30
# If QUIET is set to TRUE, don't print anything on the console (cmdline: -q)
QUIET="FALSE"
# Don't send E-mail by default (cmdline: -a)
ALARM="FALSE"
# Don't run as a Nagios plugin by default (cmdline: -n)
NAGIOS="FALSE"
# Don't summarize Nagios output by default (cmdline: -N)
NAGIOSSUMMARY="FALSE"
# NULL out the PKCSDBPASSWD variable for later use (cmdline: -k)
PKCSDBPASSWD=""
# Type of certificate (PEM, DER, NET) (cmdline: -t)
CERTTYPE="pem"
# Location of system binaries
AWK=$(command -v awk)
DATE=$(command -v date)
GREP=$(command -v grep)
OPENSSL=$(command -v openssl)
PRINTF=$(command -v printf)
SED=$(command -v sed)
MKTEMP=$(command -v mktemp)
FIND=$(command -v find)
# Try to find a mail client
if [ -f /usr/bin/mailx ]; then
MAIL="/usr/bin/mailx"
MAILMODE="mailx"
elif [ -f /bin/mail ]; then
MAIL="/bin/mail"
MAILMODE="mail"
elif [ -f /usr/bin/mail ]; then
MAIL="/usr/bin/mail"
MAILMODE="mail"
elif [ -f /sbin/mail ]; then
MAIL="/sbin/mail"
MAILMODE="mail"
elif [ -f /usr/sbin/mail ]; then
MAIL="/usr/sbin/mail"
MAILMODE="mail"
elif [ -f /usr/sbin/sendmail ]; then
MAIL="/usr/sbin/sendmail"
MAILMODE="sendmail"
else
MAIL="cantfindit"
MAILMODE="cantfindit"
fi
# Return code used by nagios. Initialize to 0.
RETCODE=0
# Certificate counters and minimum difference. Initialize to 0.
SUMMARY_VALID=0
SUMMARY_WILL_EXPIRE=0
SUMMARY_EXPIRED=0
SUMMARY_MIN_DIFF=0
SUMMARY_MIN_DATE=
SUMMARY_MIN_HOST=
SUMMARY_MIN_PORT=
# Set the default umask to be somewhat restrictive
umask 077
#####################################################
# Purpose: Remove temporary files if the script doesn't
# exit() cleanly
#####################################################
cleanup() {
if [ -f "${CERT_TMP}" ]; then
rm -f "${CERT_TMP}"
fi
if [ -f "${ERROR_TMP}" ]; then
rm -f "${ERROR_TMP}"
fi
}
#####################################################
### Send email
### Accepts three parameters:
### $1 -> sender email address
### $2 -> email to send mail
### $3 -> Subject
### $4 -> Message
#####################################################
send_mail() {
FROM="${1}"
TO="${2}"
SUBJECT="${3}"
MSG="${4}"
case "${MAILMODE}" in
"mail")
echo "$MSG" | "${MAIL}" -r "$FROM" -s "$SUBJECT" "$TO"
;;
"mailx")
echo "$MSG" | "${MAIL}" -s "$SUBJECT" "$TO"
;;
"sendmail")
(echo "Subject:$SUBJECT" && echo "TO:$TO" && echo "FROM:$FROM" && echo "$MSG") | "${MAIL}" "$TO"
;;
"*")
echo "ERROR: You enabled automated alerts, but the mail binary could not be found."
echo "FIX: Please modify the \${MAIL} and \${MAILMODE} variable in the program header."
exit 1
;;
esac
}
#############################################################################
# Purpose: Convert a date from MONTH-DAY-YEAR to Julian format
# Acknowledgements: Code was adapted from examples in the book
# "Shell Scripting Recipes: A Problem-Solution Approach"
# ( ISBN 1590594711 )
# Arguments:
# $1 -> Month (e.g., 06)
# $2 -> Day (e.g., 08)
# $3 -> Year (e.g., 2006)
#############################################################################
date2julian() {
if [ "${1}" != "" ] && [ "${2}" != "" ] && [ "${3}" != "" ]; then
## Since leap years add aday at the end of February,
## calculations are done from 1 March 0000 (a fictional year)
d2j_tmpmonth=$((12 * $3 + $1 - 3))
## If it is not yet March, the year is changed to the previous year
d2j_tmpyear=$(( d2j_tmpmonth / 12))
## The number of days from 1 March 0000 is calculated
## and the number of days from 1 Jan. 4713BC is added
echo $(( (734 * d2j_tmpmonth + 15) / 24
- 2 * d2j_tmpyear + d2j_tmpyear/4
- d2j_tmpyear/100 + d2j_tmpyear/400 + $2 + 1721119 ))
else
echo 0
fi
}
#############################################################################
# Purpose: Convert a string month into an integer representation
# Arguments:
# $1 -> Month name (e.g., Sep)
#############################################################################
getmonth()
{
case ${1} in
Jan) echo 1 ;;
Feb) echo 2 ;;
Mar) echo 3 ;;
Apr) echo 4 ;;
May) echo 5 ;;
Jun) echo 6 ;;
Jul) echo 7 ;;
Aug) echo 8 ;;
Sep) echo 9 ;;
Oct) echo 10 ;;
Nov) echo 11 ;;
Dec) echo 12 ;;
*) echo 0 ;;
esac
}
#############################################################################
# Purpose: Calculate the number of seconds between two dates
# Arguments:
# $1 -> Date #1
# $2 -> Date #2
#############################################################################
date_diff()
{
if [ "${1}" != "" ] && [ "${2}" != "" ]; then
echo $((${2} - ${1}))
else
echo 0
fi
}
#####################################################################
# Purpose: Print a line with the expiraton interval
# Arguments:
# $1 -> Hostname
# $2 -> TCP Port
# $3 -> Status of certification (e.g., expired or valid)
# $4 -> Date when certificate will expire
# $5 -> Days left until the certificate will expire
# $6 -> Issuer of the certificate
# $7 -> Common Name
# $8 -> Serial Number
#####################################################################
prints()
{
if [ "${NAGIOSSUMMARY}" = "TRUE" ]; then
return
fi
if [ "${QUIET}" != "TRUE" ] && [ "${ISSUER}" = "TRUE" ] && [ "${VALIDATION}" != "TRUE" ]; then
MIN_DATE=$(echo "$4" | "${AWK}" '{ printf "%3s %2d %4d", $1, $2, $4 }')
if [ "${NAGIOS}" = "TRUE" ]; then
${PRINTF} "%-35s %-17s %-8s %-11s %s\n" "$1:$2" "$6" "$3" "$MIN_DATE" "|days=$5"
else
${PRINTF} "%-35s %-17s %-8s %-11s %4d\n" "$1:$2" "$6" "$3" "$MIN_DATE" "$5"
fi
elif [ "${QUIET}" != "TRUE" ] && [ "${ISSUER}" = "TRUE" ] && [ "${VALIDATION}" = "TRUE" ]; then
${PRINTF} "%-35s %-35s %-32s %-17s\n" "$1:$2" "$7" "$8" "$6"
elif [ "${QUIET}" != "TRUE" ] && [ "${VALIDATION}" != "TRUE" ]; then
MIN_DATE=$(echo "$4" | "${AWK}" '{ printf "%3s %2d, %4d", $1, $2, $4 }')
if [ "${NAGIOS}" = "TRUE" ]; then
${PRINTF} "%-47s %-12s %-12s %s\n" "$1:$2" "$3" "$MIN_DATE" "|days=$5"
else
${PRINTF} "%-47s %-12s %-12s %4d\n" "$1:$2" "$3" "$MIN_DATE" "$5"
fi
elif [ "${QUIET}" != "TRUE" ] && [ "${VALIDATION}" = "TRUE" ]; then
${PRINTF} "%-35s %-35s %-32s\n" "$1:$2" "$7" "$8"
fi
}
####################################################
# Purpose: Print a heading with the relevant columns
# Arguments:
# None
####################################################
print_heading()
{
if [ "${NOHEADER}" != "TRUE" ]; then
if [ "${QUIET}" != "TRUE" ] && [ "${ISSUER}" = "TRUE" ] && [ "${NAGIOS}" != "TRUE" ] && [ "${VALIDATION}" != "TRUE" ]; then
${PRINTF} "\n%-35s %-17s %-8s %-11s %-4s\n" "Host" "Issuer" "Status" "Expires" "Days"
echo "----------------------------------- ----------------- -------- ----------- ----"
elif [ "${QUIET}" != "TRUE" ] && [ "${ISSUER}" = "TRUE" ] && [ "${NAGIOS}" != "TRUE" ] && [ "${VALIDATION}" = "TRUE" ]; then
${PRINTF} "\n%-35s %-35s %-32s %-17s\n" "Host" "Common Name" "Serial #" "Issuer"
echo "----------------------------------- ----------------------------------- -------------------------------- -----------------"
elif [ "${QUIET}" != "TRUE" ] && [ "${NAGIOS}" != "TRUE" ] && [ "${VALIDATION}" != "TRUE" ]; then
${PRINTF} "\n%-47s %-12s %-12s %-4s\n" "Host" "Status" "Expires" "Days"
echo "----------------------------------------------- ------------ ------------ ----"
elif [ "${QUIET}" != "TRUE" ] && [ "${NAGIOS}" != "TRUE" ] && [ "${VALIDATION}" = "TRUE" ]; then
${PRINTF} "\n%-35s %-35s %-32s\n" "Host" "Common Name" "Serial #"
echo "----------------------------------- ----------------------------------- --------------------------------"
fi
fi
}
####################################################
# Purpose: Print a summary for nagios
# Arguments:
# None
####################################################
print_summary()
{
if [ "${NAGIOSSUMMARY}" != "TRUE" ]; then
return
fi
if [ ${SUMMARY_WILL_EXPIRE} -eq 0 ] && [ ${SUMMARY_EXPIRED} -eq 0 ]; then
${PRINTF} "%s valid certificate(s)|days=%s\n" "${SUMMARY_VALID}" "${SUMMARY_MIN_DIFF}"
elif [ ${SUMMARY_EXPIRED} -ne 0 ]; then
${PRINTF} "%s certificate(s) expired (%s:%s on %s)|days=%s\n" "${SUMMARY_EXPIRED}" "${SUMMARY_MIN_HOST}" "${SUMMARY_MIN_PORT}" "${SUMMARY_MIN_DATE}" "${SUMMARY_MIN_DIFF}"
elif [ ${SUMMARY_WILL_EXPIRE} -ne 0 ]; then
${PRINTF} "%s certificate(s) will expire (%s:%s on %s)|days=%s\n" "${SUMMARY_WILL_EXPIRE}" "${SUMMARY_MIN_HOST}" "${SUMMARY_MIN_PORT}" "${SUMMARY_MIN_DATE}" "${SUMMARY_MIN_DIFF}"
fi
}
#############################################################
# Purpose: Set returncode to value if current value is lower
# Arguments:
# $1 -> New returncorde
#############################################################
set_returncode()
{
if [ "${RETCODE}" -lt "${1}" ]; then
RETCODE="${1}"
fi
}
########################################################################
# Purpose: Set certificate counters and informations for nagios summary
# Arguments:
# $1 -> Status of certificate (0: valid, 1: will expire, 2: expired)
# $2 -> Hostname
# $3 -> TCP Port
# $4 -> Date when certificate will expire
# $5 -> Days left until the certificate will expire
########################################################################
set_summary()
{
if [ "${1}" -eq 0 ]; then
SUMMARY_VALID=$((SUMMARY_VALID+1))
elif [ "${1}" -eq 1 ]; then
SUMMARY_WILL_EXPIRE=$((SUMMARY_WILL_EXPIRE+1))
else
SUMMARY_EXPIRED=$((SUMMARY_EXPIRED+1))
fi
if [ "${5}" -lt "${SUMMARY_MIN_DIFF}" ] || [ "${SUMMARY_MIN_DIFF}" -eq 0 ]; then
SUMMARY_MIN_DATE="${4}"
SUMMARY_MIN_DIFF="${5}"
SUMMARY_MIN_HOST="${2}"
SUMMARY_MIN_PORT="${3}"
fi
}
##########################################
# Purpose: Describe how the script works
# Arguments:
# None
##########################################
usage()
{
echo "Usage: $0 [ -e email address ] [-E sender email address] [ -x days ] [-q] [-a] [-b] [-h] [-i] [-n] [-N] [-v]"
echo " { [ -s common_name ] && [ -p port] } || { [ -f cert_file ] } || { [ -c cert file ] } || { [ -d cert dir ] }"
echo ""
echo " -a : Send a warning message through E-mail"
echo " -b : Will not print header"
echo " -c cert file : Print the expiration date for the PEM or PKCS12 formatted certificate in cert file"
echo " -d cert directory : Print the expiration date for the PEM or PKCS12 formatted certificates in cert directory"
echo " -e E-mail address : E-mail address to send expiration notices"
echo " -E E-mail sender : E-mail address of the sender"
echo " -f cert file : File with a list of FQDNs and ports"
echo " -h : Print this screen"
echo " -i : Print the issuer of the certificate"
echo " -k password : PKCS12 file password"
echo " -n : Run as a Nagios plugin"
echo " -N : Run as a Nagios plugin and output one line summary (implies -n, requires -f or -d)"
echo " -p port : Port to connect to (interactive mode)"
echo " -q : Don't print anything on the console"
echo " -s commmon name : Server to connect to (interactive mode)"
echo " -S : Print validation information"
echo " -t type : Specify the certificate type"
echo " -V : Print version information"
echo " -x days : Certificate expiration interval (eg. if cert_date < days)"
echo ""
}
##########################################################################
# Purpose: Connect to a server ($1) and port ($2) to see if a certificate
# has expired
# Arguments:
# $1 -> Server name
# $2 -> TCP port to connect to
##########################################################################
check_server_status() {
PORT="$2"
case "$PORT" in
smtp|25|submission|587) TLSFLAG="-starttls smtp";;
pop3|110) TLSFLAG="-starttls pop3";;
imap|143) TLSFLAG="-starttls imap";;
ftp|21) TLSFLAG="-starttls ftp";;
xmpp|5222) TLSFLAG="-starttls xmpp";;
xmpp-server|5269) TLSFLAG="-starttls xmpp-server";;
irc|194) TLSFLAG="-starttls irc";;
postgres|5432) TLSFLAG="-starttls postgres";;
mysql|3306) TLSFLAG="-starttls mysql";;
lmtp|24) TLSFLAG="-starttls lmtp";;
nntp|119) TLSFLAG="-starttls nntp";;
sieve|4190) TLSFLAG="-starttls sieve";;
ldap|389) TLSFLAG="-starttls ldap";;
*) TLSFLAG="";;
esac
if [ "${TLSSERVERNAME}" = "FALSE" ]; then
OPTIONS="-connect ${1}:${2} $TLSFLAG"
else
OPTIONS="-connect ${1}:${2} -servername ${1} $TLSFLAG"
fi
echo "" | "${OPENSSL}" s_client $OPTIONS 2> "${ERROR_TMP}" 1> "${CERT_TMP}"
if "${GREP}" -i "Connection refused" "${ERROR_TMP}" > /dev/null; then
prints "${1}" "${2}" "Connection refused" "Unknown"
set_returncode 3
elif "${GREP}" -i "No route to host" "${ERROR_TMP}" > /dev/null; then
prints "${1}" "${2}" "No route to host" "Unknown"
set_returncode 3
elif "${GREP}" -i "gethostbyname failure" "${ERROR_TMP}" > /dev/null; then
prints "${1}" "${2}" "Cannot resolve domain" "Unknown"
set_returncode 3
elif "${GREP}" -i "Operation timed out" "${ERROR_TMP}" > /dev/null; then
prints "${1}" "${2}" "Operation timed out" "Unknown"
set_returncode 3
elif "${GREP}" -i "ssl handshake failure" "${ERROR_TMP}" > /dev/null; then
prints "${1}" "${2}" "SSL handshake failed" "Unknown"
set_returncode 3
elif "${GREP}" -i "connect: Connection timed out" "${ERROR_TMP}" > /dev/null; then
prints "${1}" "${2}" "Connection timed out" "Unknown"
set_returncode 3
elif "${GREP}" -i "Name or service not known" "${ERROR_TMP}" > /dev/null; then
prints "${1}" "${2}" "Unable to resolve the DNS name ${1}" "Unknown"
set_returncode 3
else
check_file_status "${CERT_TMP}" "${1}" "${2}"
fi
}
#####################################################
### Check the expiration status of a certificate file
### Accepts three parameters:
### $1 -> certificate file to process
### $2 -> Server name
### $3 -> Port number of certificate
#####################################################
check_file_status() {
CERTFILE="${1}"
HOST="${2}"
PORT="${3}"
### Check to make sure the certificate file exists
if [ ! -r "${CERTFILE}" ] || [ ! -s "${CERTFILE}" ]; then
echo "ERROR: The file named ${CERTFILE} is unreadable or doesn't exist"
echo "ERROR: Please check to make sure the certificate for ${HOST}:${PORT} is valid"
set_returncode 3
return
fi
### Grab the expiration date from the X.509 certificate
if [ "${PKCSDBPASSWD}" != "" ]; then
# Extract the certificate from the PKCS#12 database, and
# send the informational message to /dev/null
"${OPENSSL}" pkcs12 -nokeys -in "${CERTFILE}" \
-out "${CERT_TMP}" -clcerts -password pass:"${PKCSDBPASSWD}" 2> /dev/null
# Extract the expiration date from the certificate
CERTDATE=$("${OPENSSL}" x509 -in "${CERT_TMP}" -enddate -noout | \
"${SED}" 's/notAfter\=//')
# Extract the issuer from the certificate
CERTISSUER=$("${OPENSSL}" x509 -in "${CERT_TMP}" -issuer -noout | \
"${AWK}" 'BEGIN {RS=", " } $0 ~ /^O =/
{ print substr($0,5,17)}')
### Grab the common name (CN) from the X.509 certificate
COMMONNAME=$("${OPENSSL}" x509 -in "${CERT_TMP}" -subject -noout | \
"${SED}" -e 's/.*CN = //' | \
"${SED}" -e 's/, .*//')
### Grab the serial number from the X.509 certificate
SERIAL=$("${OPENSSL}" x509 -in "${CERT_TMP}" -serial -noout | \
"${SED}" -e 's/serial=//')
else
# Extract the expiration date from the ceriticate
CERTDATE=$("${OPENSSL}" x509 -in "${CERTFILE}" -enddate -noout -inform "${CERTTYPE}" | \
"${SED}" 's/notAfter\=//')
# Extract the issuer from the certificate
CERTISSUER=$("${OPENSSL}" x509 -in "${CERTFILE}" -issuer -noout -inform "${CERTTYPE}" | \
"${AWK}" 'BEGIN {RS=", " } $0 ~ /^O =/ { print substr($0,5,17)}')
### Grab the common name (CN) from the X.509 certificate
COMMONNAME=$("${OPENSSL}" x509 -in "${CERTFILE}" -subject -noout -inform "${CERTTYPE}" | \
"${SED}" -e 's/.*CN = //' | \
"${SED}" -e 's/, .*//')
### Grab the serial number from the X.509 certificate
SERIAL=$("${OPENSSL}" x509 -in "${CERTFILE}" -serial -noout -inform "${CERTTYPE}" | \
"${SED}" -e 's/serial=//')
fi
### Split the result into parameters, and pass the relevant pieces to date2julian
set -- ${CERTDATE}
MONTH=$(getmonth "${1}")
# Convert the date to seconds, and get the diff between NOW and the expiration date
CERTJULIAN=$(date2julian "${MONTH#0}" "${2#0}" "${4}")
CERTDIFF=$(date_diff "${NOWJULIAN}" "${CERTJULIAN}")
if [ "${CERTDIFF}" -lt 0 ]; then
if [ "${ALARM}" = "TRUE" ]; then
send_mail "${SENDER}" "${ADMIN}" "Certificate for ${HOST} \"(CN: ${COMMONNAME})\" has expired!" \
"The SSL certificate for ${HOST} \"(CN: ${COMMONNAME})\" has expired!"
fi
prints "${HOST}" "${PORT}" "Expired" "${CERTDATE}" "${CERTDIFF}" "${CERTISSUER}" "${COMMONNAME}" "${SERIAL}"
RETCODE_LOCAL=2
elif [ "${CERTDIFF}" -lt "${WARNDAYS}" ]; then
if [ "${ALARM}" = "TRUE" ]; then
send_mail "${SENDER}" "${ADMIN}" "Certificate for ${HOST} \"(CN: ${COMMONNAME})\" will expire in ${CERTDIFF} days or less" \
"The SSL certificate for ${HOST} \"(CN: ${COMMONNAME})\" will expire on ${CERTDATE}"
fi
prints "${HOST}" "${PORT}" "Expiring" "${CERTDATE}" "${CERTDIFF}" "${CERTISSUER}" "${COMMONNAME}" "${SERIAL}"
RETCODE_LOCAL=1
else
prints "${HOST}" "${PORT}" "Valid" "${CERTDATE}" "${CERTDIFF}" "${CERTISSUER}" "${COMMONNAME}" "${SERIAL}"
RETCODE_LOCAL=0
fi
set_returncode "${RETCODE_LOCAL}"
MIN_DATE=$(echo "${CERTDATE}" | "${AWK}" '{ print $1, $2, $4 }')
set_summary "${RETCODE_LOCAL}" "${HOST}" "${PORT}" "${MIN_DATE}" "${CERTDIFF}"
}
#################################
### Start of main program
#################################
while getopts abc:d:e:E:f:hik:nNp:qs:St:Vx: option
do
case "${option}" in
a) ALARM="TRUE";;
b) NOHEADER="TRUE";;
c) CERTFILE=${OPTARG};;
d) CERTDIRECTORY=${OPTARG};;
e) ADMIN=${OPTARG};;
E) SENDER=${OPTARG};;
f) SERVERFILE=$OPTARG;;
h) usage
exit 1;;
i) ISSUER="TRUE";;
k) PKCSDBPASSWD=${OPTARG};;
n) NAGIOS="TRUE";;
N) NAGIOS="TRUE"
NAGIOSSUMMARY="TRUE";;
p) PORT=$OPTARG;;
q) QUIET="TRUE";;
s) HOST=$OPTARG;;
S) VALIDATION="TRUE";;
t) CERTTYPE=$OPTARG;;
V) echo "${PROGRAMVERSION}"
exit 0
;;
x) WARNDAYS=$OPTARG;;
\?) usage
exit 1;;
esac
done
### Check to make sure a openssl utility is available
if [ ! -f "${OPENSSL}" ]; then
echo "ERROR: The openssl binary does not exist in ${OPENSSL}."
echo "FIX: Please modify the \${OPENSSL} variable in the program header."
exit 1
fi
### Check to make sure a date utility is available
if [ ! -f "${DATE}" ]; then
echo "ERROR: The date binary does not exist in ${DATE} ."
echo "FIX: Please modify the \${DATE} variable in the program header."
exit 1
fi
### Check to make sure a grep and find utility is available
if [ ! -f "${GREP}" ] || [ ! -f "${FIND}" ]; then
echo "ERROR: Unable to locate the grep and find binary."
echo "FIX: Please modify the \${GREP} and \${FIND} variables in the program header."
exit 1
fi
### Check to make sure the mktemp and printf utilities are available
if [ ! -f "${MKTEMP}" ] || [ -z "${PRINTF}" ]; then
echo "ERROR: Unable to locate the mktemp or printf binary."
echo "FIX: Please modify the \${MKTEMP} and \${PRINTF} variables in the program header."
exit 1
fi
### Check to make sure the sed and awk binaries are available
if [ ! -f "${SED}" ] || [ ! -f "${AWK}" ]; then
echo "ERROR: Unable to locate the sed or awk binary."
echo "FIX: Please modify the \${SED} and \${AWK} variables in the program header."
exit 1
fi
### Check to make sure a mail client is available it automated notifications are requested
if [ "${ALARM}" = "TRUE" ] && [ ! -f "${MAIL}" ]; then
echo "ERROR: You enabled automated alerts, but the mail binary could not be found."
echo "FIX: Please modify the ${MAIL} variable in the program header."
exit 1
fi
# Send along the servername when TLS is used
if ${OPENSSL} s_client -help 2>&1 | grep '-servername' > /dev/null; then
TLSSERVERNAME="TRUE"
else
TLSSERVERNAME="FALSE"
fi
# Place to stash temporary files
CERT_TMP=$($MKTEMP /var/tmp/cert.XXXXXX)
ERROR_TMP=$($MKTEMP /var/tmp/error.XXXXXX)
### Baseline the dates so we have something to compare to
MONTH=$(${DATE} "+%m")
DAY=$(${DATE} "+%d")
YEAR=$(${DATE} "+%Y")
NOWJULIAN=$(date2julian "${MONTH#0}" "${DAY#0}" "${YEAR}")
### Touch the files prior to using them
if [ -n "${CERT_TMP}" ] && [ -n "${ERROR_TMP}" ]; then
touch "${CERT_TMP}" "${ERROR_TMP}"
else
echo "ERROR: Problem creating temporary files"
echo "FIX: Check that mktemp works on your system"
exit 1
fi
### If a HOST was passed on the cmdline, use that value
if [ "${HOST}" != "" ]; then
print_heading
check_server_status "${HOST}" "${PORT:=443}"
print_summary
### If a file is passed to the "-f" option on the command line, check
### each certificate or server / port combination in the file to see if
### they are about to expire
elif [ -f "${SERVERFILE}" ]; then
print_heading
IFS=$'\n'
for LINE in $(grep -E -v '(^#|^$)' "${SERVERFILE}")
do
HOST=${LINE%% *}
PORT=${LINE##* }
IFS=" "
if [ "$PORT" = "FILE" ]; then
check_file_status "${HOST}" "FILE" "${HOST}"
else
check_server_status "${HOST}" "${PORT}"
fi
done
IFS="${OLDIFS}"
print_summary
### Check to see if the certificate in CERTFILE is about to expire
elif [ "${CERTFILE}" != "" ]; then
print_heading
check_file_status "${CERTFILE}" "FILE" "${CERTFILE}"
print_summary
### Check to see if the certificates in CERTDIRECTORY are about to expire
elif [ "${CERTDIRECTORY}" != "" ] && ("${FIND}" -L "${CERTDIRECTORY}" -type f > /dev/null 2>&1); then
print_heading
for FILE in $("${FIND}" -L "${CERTDIRECTORY}" -type f); do
check_file_status "${FILE}" "FILE" "${FILE}"
done
print_summary
### There was an error, so print a detailed usage message and exit
else
usage
exit 1
fi
### Exit with a success indicator
if [ "${NAGIOS}" = "TRUE" ]; then
exit "${RETCODE}"
else
exit 0
fi

View File

@@ -16,7 +16,7 @@ more_set_headers "X-Frame-Options: SAMEORIGIN";
more_set_headers "X-Content-Type-Options: nosniff";
more_set_headers "Referrer-Policy: no-referrer-when-downgrade";
# These are dummy restrictions (meaningless headers) below.
more_set_headers "Content-Security-Policy: img-src *";
more_set_headers "Content-Security-Policy: img-src * data: blob:; ";
more_set_headers "Permissions-Policy: geolocation=*";
# https://amalgjose.com/2020/05/15/how-to-set-the-allowed-url-length-for-a-nginx-request-error-code-414-uri-too-large/

View File

@@ -23,7 +23,7 @@ server {
# Let's Encrypt (acme.sh) support.
location ~ ^(/\.well-known/(?!(caldav|carddav|nodeinfo|webfinger)).*$) {
proxy_pass http://localhost:8100;
proxy_pass http://$PAR_ACMEHOST:$PAR_ACMEPORT;
error_page 500 502 503 504 @proxy_error;
}

View File

@@ -1,9 +1,8 @@
# This is a shell script excerpt for configuration purposes only.
# Handle with care! Please don't put code here, only variables.
CLASSES_PATTERN="^([^.]*)\..*\.$HOSTNAME\.(dmp|sql\.gz|tgz|log)$"
CLASSES_PATTERN="^([^.]*)\..*\.$HOSTNAME\.(dmp|sql\.gz|tar|tgz|log)$"
DOIT="yes" # if empty the script makes a dry run
RETAIN_DAYS=7 # retains all files created within that many days
RETAIN_DAYS=1 # retains all files created within that many days
RETAIN_WEEKS=0 # retains one file per week/month,
RETAIN_MONTHS=0 # created within that many weeks/months

View File

@@ -1,9 +1,8 @@
# This is a shell script excerpt for configuration purposes only.
# Handle with care! Please don't put code here, only variables.
CLASSES_PATTERN="^([^.]*)\..*\.$HOSTNAME\.(dmp|sql\.gz|tgz|log)$"
CLASSES_PATTERN="^([^.]*)\..*\.$HOSTNAME\.(dmp|sql\.gz|tar|tgz|log)$"
DOIT="yes" # if empty the script makes a dry run
# RETAIN_DAYS=7 # retains all files created within that many days
# RETAIN_WEEKS=4 # retains one file per week/month,
# RETAIN_MONTHS=12 # created within that many weeks/months

95
tools/check_certificates Executable file
View File

@@ -0,0 +1,95 @@
#!/bin/bash
#
# A humble script to check the expiration of this web service certificates.
# It uses the ssl-cert-check worker utility, which should be somewhere in path.
# Sends an email notification when any certificate is about to expire.
# See https://github.com/Matty9191/ssl-cert-check for details.
#
# Author: Kovács Zoltán <kovacsz@marcusconsulting.hu>
# 2026-02-16 v0.1 Initial release
# Accepted environment variables and their defaults.
PAR_BASEDIR=${PAR_BASEDIR:-""} # Service's base folder
# Other initialisations.
ALERTEMAIL="$USER@$HOSTNAME" # Alert notification recipient
ALERTEXPIRY=14 # Days to alert before expiry
CERTFILES="-name *.cer -o -name *.crt -o -name *.pem"
CERTFOLDERS="configs/acme configs/certs" # Files and folders to check
WORKERNAME="ssl-cert-check" # The 3rd party worker script
YMLFILE="docker-compose.yml"
# Messages.
MSG_MISSINGDEP="Fatal: missing dependency"
MSG_MISSINGYML="Fatal: didn't find the docker-compose.yml file"
# Basic environment settings.
LANG=C
LC_ALL=C
# Checks the dependencies.
TR=$(which tr 2>/dev/null)
if [ -z "$TR" ]; then echo "$MSG_MISSINGDEP tr."; exit 1 ; fi
for item in basename dirname find readlink
do
if [ -n "$(which $item)" ]
then export $(echo $item | "$TR" '[:lower:]' '[:upper:]' | "$TR" '-' '_')=$(which $item)
else echo "$MSG_MISSINGDEP $item." >&2; exit 1; fi
done
# All dependencies are available via "$THECOMMAND" (upper case) call.
# Where I'm?
# https://gist.github.com/TheMengzor/968e5ea87e99d9c41782
SOURCE="${BASH_SOURCE[0]}"
while [ -h "$SOURCE" ]; do
# resolve $SOURCE until the file is no longer a symlink
SCRPATH="$( cd -P "$( "$DIRNAME" "$SOURCE" )" && pwd )" #"
SOURCE="$("$READLINK" "$SOURCE")"
# if $SOURCE was a relative symlink, we need to resolve it
# relative to the path where the symlink file was located
[[ $SOURCE != /* ]] && SOURCE="$SCRPATH/$SOURCE"
done; SCRPATH="$( cd -P "$( "$DIRNAME" "$SOURCE" )" && pwd )" #"
SCRFILE="$("$BASENAME" "$(test -L "$0" && "$READLINK" "$0" || echo "$0")")" #"
# Searches the base folder, containing a docker-compose.yml file.
# Called from the base folder (./)?
BASE_DIR="$PAR_BASEDIR"
TEST_DIR="$SCRPATH"
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
# Called from ./tools?
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
# Called from ./tools/*.d?
TEST_DIR="$("$DIRNAME" "$TEST_DIR")"
[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR"
# On failure gives it up here.
if [ -z "$BASE_DIR" -o ! -r "$BASE_DIR/$YMLFILE" ]; then
echo "$MSG_MISSINGYML" >&2; exit 1
fi
# Locates the worker script.
WORKERSCRIPT="$SCRPATH/$WORKERNAME"
[[ ! -x "$WORKERSCRIPT" ]] && WORKERSCRIPT="$(which "$WORKERNAME")"
[[ ! -x "$WORKERSCRIPT" ]] \
&& echo -e "$MSG_MISSINGDEP $WORKERNAME." >&2 \
&& exit 1
# Collects the certificates to be check.
certificates=""
for folder in $CERTFOLDERS ""
do
if [ -n "$folder" ]; then
certificates+="$("$FIND" "$BASE_DIR/$folder" \( $CERTFILES \) 2>/dev/null) "
fi
done
# Enumerates and checks the collected certificates.
# Sends an email notification when expiration is approaching.
for cert in $certificates ""
do
if [ -n "$cert" ]; then
"$WORKERSCRIPT" -c "$cert" -x $ALERTEXPIRY -qae "$ALERTEMAIL"
fi
done
# That's all, Folks! :)

View File

@@ -10,8 +10,10 @@
# Author: Kovács Zoltán <kovacs.zoltan@smartfront.hu>
# Kovács Zoltán <kovacsz@marcusconsulting.hu>
# License: GNU/GPL v3+ (https://www.gnu.org/licenses/gpl-3.0.en.html)
# 2026-02-16 v1.1
# new: It now also calls the SSL web certificate expiration checker utility.
# 2023-06-18 v1.0
# new: forked from the "Smartfront's DOCKER_skeleton" repository.
# new: Forked from the "Smartfront's DOCKER_skeleton" repository.
# 2021-09-14 v0.2
# add: Checks the SSL web certificate (if any), renews it if necessary.
# 2021-09-01 v0.1 Initial release
@@ -24,7 +26,7 @@
# Where I'm?
SCRPATH="$( cd -P "$( "$(which dirname)" "$0" )" && echo "$PWD" )"
# Checks the SSL web certificate, renews it if necessery.
# Checks the ACME-handled SSL web certificates, renews them if necessery.
#
# Uses the acme wrapper script located in the same directory.
ACME="$SCRPATH/acme"
@@ -32,7 +34,17 @@ ACMELOG="$($(which dirname) "$SCRPATH")/logs/web/acme.log"
if [ -n "$ACME" -a -x "$ACME" ]; then
"$ACME" --cron >> "$ACMELOG" 2>&1
fi
# Done with the certificate.
# Done with the ACME certificates.
# Checks all SSL web certificates (ACME-handled or not) for expiration.
# Checks the expiration of all certificates (including ACME-handled
# and non-handled ones).
#
CHECKCERT="$SCRPATH/check_certificates"
if [ -n "$CHECKCERT" -a -x "$CHECKCERT" ]; then
"$CHECKCERT"
fi
# Done with certificates.
# Daily backup operations.
#