first commit
This commit is contained in:
commit
4b9ffa9e08
53
Dockerfile
Normal file
53
Dockerfile
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
FROM golang:1.16.7-alpine AS go-builder
|
||||||
|
|
||||||
|
ENV DOCKER_GEN_VERSION=0.7.6
|
||||||
|
|
||||||
|
# Build docker-gen
|
||||||
|
RUN apk add --no-cache --virtual .build-deps git \
|
||||||
|
&& git clone https://github.com/nginx-proxy/docker-gen \
|
||||||
|
&& cd /go/docker-gen \
|
||||||
|
&& git -c advice.detachedHead=false checkout $DOCKER_GEN_VERSION \
|
||||||
|
&& go mod download \
|
||||||
|
&& CGO_ENABLED=0 go build -ldflags "-X main.buildVersion=${VERSION}" -o docker-gen ./cmd/docker-gen \
|
||||||
|
&& go clean -cache \
|
||||||
|
&& mv docker-gen /usr/local/bin/ \
|
||||||
|
&& cd - \
|
||||||
|
&& rm -rf /go/docker-gen \
|
||||||
|
&& apk del .build-deps
|
||||||
|
|
||||||
|
FROM alpine:3.13.5
|
||||||
|
|
||||||
|
LABEL maintainer="Nicolas Duchon <nicolas.duchon@gmail.com> (@buchdag)"
|
||||||
|
|
||||||
|
ARG GIT_DESCRIBE
|
||||||
|
ARG ACMESH_VERSION=2.8.8
|
||||||
|
|
||||||
|
ENV COMPANION_VERSION=$GIT_DESCRIBE \
|
||||||
|
DOCKER_HOST=unix:///var/run/docker.sock \
|
||||||
|
PATH=$PATH:/app
|
||||||
|
|
||||||
|
# Install packages required by the image
|
||||||
|
RUN apk add --no-cache --virtual .bin-deps \
|
||||||
|
bash \
|
||||||
|
coreutils \
|
||||||
|
curl \
|
||||||
|
jq \
|
||||||
|
openssl \
|
||||||
|
socat
|
||||||
|
|
||||||
|
# Install docker-gen from build stage
|
||||||
|
COPY --from=go-builder /usr/local/bin/docker-gen /usr/local/bin/
|
||||||
|
|
||||||
|
# Install acme.sh
|
||||||
|
COPY /install_acme.sh /app/install_acme.sh
|
||||||
|
RUN chmod +rx /app/install_acme.sh \
|
||||||
|
&& sync \
|
||||||
|
&& /app/install_acme.sh \
|
||||||
|
&& rm -f /app/install_acme.sh
|
||||||
|
|
||||||
|
COPY /app/ /app/
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENTRYPOINT [ "/bin/bash", "/app/entrypoint.sh" ]
|
||||||
|
CMD [ "/bin/bash", "/app/start.sh" ]
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015-2016 Yves Blusseau
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
117
README.md
Normal file
117
README.md
Normal file
@ -0,0 +1,117 @@
|
|||||||
|

|
||||||
|
[](https://github.com/nginx-proxy/acme-companion/releases)
|
||||||
|
[](https://hub.docker.com/r/nginxproxy/acme-companion "Click to view the image on Docker Hub")
|
||||||
|
[](https://hub.docker.com/r/nginxproxy/acme-companion "Click to view the image on Docker Hub")
|
||||||
|
[](https://hub.docker.com/r/nginxproxy/acme-companion "Click to view the image on Docker Hub")
|
||||||
|
|
||||||
|
**acme-companion** is a lightweight companion container for [**nginx-proxy**](https://github.com/nginx-proxy/nginx-proxy).
|
||||||
|
|
||||||
|
It handles the automated creation, renewal and use of SSL certificates for proxied Docker containers through the ACME protocol.
|
||||||
|
|
||||||
|
**Required read if you use the `latest` version** : the `v2.0.0` release of this project mark the switch of the ACME client used by the Docker image from [**simp.le**](https://github.com/zenhack/simp_le) to [**acme.sh**](https://github.com/acmesh-official/acme.sh). This switch result in some backward incompatible changes, so please read [this issue](https://github.com/nginx-proxy/acme-companion/issues/510) and the updated docs for more details before updating your image. The single most important change is that the container now requires a volume mounted to `/etc/acme.sh` in order to persist ACME account keys and SSL certificates. The last tagged version that uses **simp_le** is `v1.13.1`.
|
||||||
|
|
||||||
|
### Features:
|
||||||
|
* Automated creation/renewal of Let's Encrypt (or other ACME CAs) certificates using [**acme.sh**](https://github.com/acmesh-official/acme.sh).
|
||||||
|
* Let's Encrypt / ACME domain validation through `http-01` challenge only.
|
||||||
|
* Automated update and reload of nginx config on certificate creation/renewal.
|
||||||
|
* Support creation of [Multi-Domain (SAN) Certificates](https://github.com/nginx-proxy/acme-companion/blob/main/docs/Let's-Encrypt-and-ACME.md#multi-domains-certificates).
|
||||||
|
* Creation of a Strong Diffie-Hellman Group at startup.
|
||||||
|
* Work with all versions of docker.
|
||||||
|
|
||||||
|
### Requirements:
|
||||||
|
* Your host **must** be publicly reachable on **both** port `80` and `443`.
|
||||||
|
* Check your firewall rules and **do not attempt to block port `80`** as that will prevent `http-01` challenges from completing.
|
||||||
|
* For the same reason, you can't use nginx-proxy's [`HTTPS_METHOD=nohttp`](https://github.com/nginx-proxy/nginx-proxy#how-ssl-support-works).
|
||||||
|
* The (sub)domains you want to issue certificates for must correctly resolve to the host.
|
||||||
|
* Your DNS provider must [answer correctly to CAA record requests](https://letsencrypt.org/docs/caa/).
|
||||||
|
* If your (sub)domains have AAAA records set, the host must be publicly reachable over IPv6 on port `80` and `443`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Basic usage (with the nginx-proxy container)
|
||||||
|
|
||||||
|
Three writable volumes must be declared on the **nginx-proxy** container so that they can be shared with the **acme-companion** container:
|
||||||
|
|
||||||
|
* `/etc/nginx/certs` to store certificates and private keys (readonly for the **nginx-proxy** container).
|
||||||
|
* `/etc/nginx/vhost.d` to change the configuration of vhosts (required so the CA may access `http-01` challenge files).
|
||||||
|
* `/usr/share/nginx/html` to write `http-01` challenge files.
|
||||||
|
|
||||||
|
Additionally, a fourth volume must be declared on the **acme-companion** container to store `acme.sh` configuration and state: `/etc/acme.sh`.
|
||||||
|
|
||||||
|
Please also read the doc about [data persistence](./docs/Persistent-data.md).
|
||||||
|
|
||||||
|
Example of use:
|
||||||
|
|
||||||
|
### Step 1 - nginx-proxy
|
||||||
|
|
||||||
|
Start **nginx-proxy** with the three additional volumes declared:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ docker run --detach \
|
||||||
|
--name nginx-proxy \
|
||||||
|
--publish 80:80 \
|
||||||
|
--publish 443:443 \
|
||||||
|
--volume certs:/etc/nginx/certs \
|
||||||
|
--volume vhost:/etc/nginx/vhost.d \
|
||||||
|
--volume html:/usr/share/nginx/html \
|
||||||
|
--volume /var/run/docker.sock:/tmp/docker.sock:ro \
|
||||||
|
nginxproxy/nginx-proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
Binding the host docker socket (`/var/run/docker.sock`) inside the container to `/tmp/docker.sock` is a requirement of **nginx-proxy**.
|
||||||
|
|
||||||
|
### Step 2 - acme-companion
|
||||||
|
|
||||||
|
Start the **acme-companion** container, getting the volumes from **nginx-proxy** with `--volumes-from`:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ docker run --detach \
|
||||||
|
--name nginx-proxy-acme \
|
||||||
|
--volumes-from nginx-proxy \
|
||||||
|
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||||
|
--volume acme:/etc/acme.sh \
|
||||||
|
--env "DEFAULT_EMAIL=mail@yourdomain.tld" \
|
||||||
|
nginxproxy/acme-companion
|
||||||
|
```
|
||||||
|
|
||||||
|
The host docker socket has to be bound inside this container too, this time to `/var/run/docker.sock`.
|
||||||
|
|
||||||
|
Albeit **optional**, it is **recommended** to provide a valid default email address through the `DEFAULT_EMAIL` environment variable, so that Let's Encrypt can warn you about expiring certificates and allow you to recover your account.
|
||||||
|
|
||||||
|
### Step 3 - proxied container(s)
|
||||||
|
|
||||||
|
Once both **nginx-proxy** and **acme-companion** containers are up and running, start any container you want proxied with environment variables `VIRTUAL_HOST` and `LETSENCRYPT_HOST` both set to the domain(s) your proxied container is going to use.
|
||||||
|
|
||||||
|
[`VIRTUAL_HOST`](https://github.com/nginx-proxy/nginx-proxy#usage) control proxying by **nginx-proxy** and `LETSENCRYPT_HOST` control certificate creation and SSL enabling by **acme-companion**.
|
||||||
|
|
||||||
|
Certificates will only be issued for containers that have both `VIRTUAL_HOST` and `LETSENCRYPT_HOST` variables set to domain(s) that correctly resolve to the host, provided the host is publicly reachable.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ docker run --detach \
|
||||||
|
--name your-proxied-app \
|
||||||
|
--env "VIRTUAL_HOST=subdomain.yourdomain.tld" \
|
||||||
|
--env "LETSENCRYPT_HOST=subdomain.yourdomain.tld" \
|
||||||
|
nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
The containers being proxied must expose the port to be proxied, either by using the `EXPOSE` directive in their Dockerfile or by using the `--expose` flag to `docker run` or `docker create`.
|
||||||
|
|
||||||
|
If the proxied container listen on and expose another port than the default `80`, you can force **nginx-proxy** to use this port with the [`VIRTUAL_PORT`](https://github.com/nginx-proxy/nginx-proxy#multiple-ports) environment variable.
|
||||||
|
|
||||||
|
Example using [Grafana](https://hub.docker.com/r/grafana/grafana/) (expose and listen on port 3000):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ docker run --detach \
|
||||||
|
--name grafana \
|
||||||
|
--env "VIRTUAL_HOST=othersubdomain.yourdomain.tld" \
|
||||||
|
--env "VIRTUAL_PORT=3000" \
|
||||||
|
--env "LETSENCRYPT_HOST=othersubdomain.yourdomain.tld" \
|
||||||
|
--env "LETSENCRYPT_EMAIL=mail@yourdomain.tld" \
|
||||||
|
grafana/grafana
|
||||||
|
```
|
||||||
|
|
||||||
|
Repeat [Step 3](#step-3---proxied-containers) for any other container you want to proxy.
|
||||||
|
|
||||||
|
## Additional documentation
|
||||||
|
|
||||||
|
Please check the [docs section](https://github.com/nginx-proxy/acme-companion/tree/main/docs).
|
||||||
74
app/cert_status
Executable file
74
app/cert_status
Executable file
@ -0,0 +1,74 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
function print_cert_info {
|
||||||
|
local enddate
|
||||||
|
local subject
|
||||||
|
local san_str
|
||||||
|
|
||||||
|
# Get the wanted informations with OpenSSL.
|
||||||
|
issuer="$(openssl x509 -noout -issuer -in "$1" | sed -n 's/.*CN = \(.*\)/\1/p')"
|
||||||
|
enddate="$(openssl x509 -noout -enddate -in "$1" | sed -n 's/notAfter=\(.*$\)/\1/p')"
|
||||||
|
subject="$(openssl x509 -noout -subject -in "$1" | sed -n 's/.*CN = \([a-z0-9.-]*\)/- \1/p')"
|
||||||
|
san_str="$(openssl x509 -text -in "$1" | grep 'DNS:')"
|
||||||
|
|
||||||
|
echo "Certificate was issued by $issuer"
|
||||||
|
if [[ "$2" == "expired" ]]; then
|
||||||
|
echo "Certificate was valid until $enddate"
|
||||||
|
else
|
||||||
|
echo "Certificate is valid until $enddate"
|
||||||
|
fi
|
||||||
|
echo "Subject Name:"
|
||||||
|
echo "$subject"
|
||||||
|
|
||||||
|
# Display the SAN info only if there is more than one SAN domain.
|
||||||
|
while IFS=',' read -ra SAN; do
|
||||||
|
if [[ ${#SAN[@]} -gt 1 ]]; then
|
||||||
|
echo "Subject Alternative Name:"
|
||||||
|
for domain in "${SAN[@]}"; do
|
||||||
|
echo "$domain" | sed -n 's/.*DNS:\([a-z0-9.-]*\)/- \1/p'
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
done <<< "$san_str"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '##### Certificate status #####'
|
||||||
|
for cert in /etc/nginx/certs/*/fullchain.pem; do
|
||||||
|
[[ -e "$cert" ]] || continue
|
||||||
|
if [[ -e "${cert%fullchain.pem}chain.pem" ]]; then
|
||||||
|
# Verify the certificate with OpenSSL.
|
||||||
|
if verify=$(openssl verify -CAfile "${cert%fullchain.pem}chain.pem" "$cert" 2>&1); then
|
||||||
|
echo "$verify"
|
||||||
|
# Print certificate info.
|
||||||
|
print_cert_info "$cert"
|
||||||
|
else
|
||||||
|
echo "${cert}: EXPIRED"
|
||||||
|
# Print certificate info.
|
||||||
|
print_cert_info "$cert" "expired"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "${cert}: no corresponding chain.pem file, unable to verify certificate"
|
||||||
|
# Print certificate info.
|
||||||
|
print_cert_info "$cert"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find the .crt files in /etc/nginx/certs which are
|
||||||
|
# symlinks pointing to the current certificate.
|
||||||
|
unset symlinked_domains
|
||||||
|
for symlink in /etc/nginx/certs/*.crt; do
|
||||||
|
[[ -e "$symlink" ]] || continue
|
||||||
|
if [[ "$(readlink -f "$symlink")" == "$cert" ]]; then
|
||||||
|
domain="${symlink%.crt}"
|
||||||
|
domain="${domain//\/etc\/nginx\/certs\//}"
|
||||||
|
symlinked_domains+=("$domain")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Display symlinks pointing to the current cert if there is any.
|
||||||
|
if [[ ${#symlinked_domains[@]} -gt 0 ]]; then
|
||||||
|
echo "Certificate is used by the following domain(s):"
|
||||||
|
for domain in "${symlinked_domains[@]}"; do
|
||||||
|
echo "- $domain"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo '##############################'
|
||||||
|
done
|
||||||
46
app/cleanup_test_artifacts
Executable file
46
app/cleanup_test_artifacts
Executable file
@ -0,0 +1,46 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# This script should not be run outside of a test container
|
||||||
|
[[ "$TEST_MODE" == 'true' ]] || exit 1
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
flag="$1"
|
||||||
|
|
||||||
|
case $flag in
|
||||||
|
--default-cert)
|
||||||
|
for filename in default.crt default.key; do
|
||||||
|
filepath="/etc/nginx/certs/$filename"
|
||||||
|
[[ -f "$filepath" ]] && rm -rf "$filepath"
|
||||||
|
done
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
|
||||||
|
--location-config)
|
||||||
|
for domain in 'le1.wtf' '*.example.com' 'test.*' 'le3.pizza' 'subdomain.example.com' 'test.domain.tld'; do
|
||||||
|
[[ -f "/etc/nginx/vhost.d/$domain" ]] && rm -f "/etc/nginx/vhost.d/$domain"
|
||||||
|
done
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
|
||||||
|
*) #Unknown option
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
for domain in le1.wtf le2.wtf le3.wtf le4.wtf lim.it; do
|
||||||
|
folder="/etc/nginx/certs/$domain"
|
||||||
|
[[ -d "$folder" ]] && rm -rf "$folder"
|
||||||
|
folder="/etc/acme.sh/default/$domain"
|
||||||
|
[[ -d "$folder" ]] && rm -rf "$folder"
|
||||||
|
folder="/etc/acme.sh/default/${domain}_ecc"
|
||||||
|
[[ -d "$folder" ]] && rm -rf "$folder"
|
||||||
|
location_file="/etc/nginx/vhost.d/$domain"
|
||||||
|
[[ -f "$location_file" ]] && rm -rf "$location_file" 2> /dev/null
|
||||||
|
for extension in key crt chain.pem dhparam.pem; do
|
||||||
|
symlink="/etc/nginx/certs/${domain}.${extension}"
|
||||||
|
[[ -L "$symlink" ]] && rm -rf "$symlink"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
exit 0
|
||||||
8
app/dhparam.pem.default
Normal file
8
app/dhparam.pem.default
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
-----BEGIN DH PARAMETERS-----
|
||||||
|
MIIBCAKCAQEAwpR+yYapElMV4DiO+BwKK2N8Ur4giZtga+dslyDMuhY+U4t/97Eq
|
||||||
|
gdFg2RD5nqrgWCRWEYcbh1kPBOAPWXZ4+N8mZL8pJXaNi2XFA8IxQex283Sz7CX+
|
||||||
|
qr/zb+piJLx+/6JB/NNTZtKurM3ZQgwdGqSHqeWgvRIgCQAykC1oz7muCsev1IMc
|
||||||
|
rLig1kyvhg3L1t+uKYV0OtiXONmPglPm9pXRqMQ53Rg/D3CpUpyyTSugOFjVhLrP
|
||||||
|
Ow+kO6qXBQSDhrL2L0UjprbcVMPHv9bFmWNoTCtC8OYA1OuiA368PWhgeH/76Yu8
|
||||||
|
4an6/vt3HowDZHKfB3Vb1VwTI+k6hzwhkwIBAg==
|
||||||
|
-----END DH PARAMETERS-----
|
||||||
173
app/entrypoint.sh
Executable file
173
app/entrypoint.sh
Executable file
@ -0,0 +1,173 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
# shellcheck source=functions.sh
|
||||||
|
source /app/functions.sh
|
||||||
|
|
||||||
|
function print_version {
|
||||||
|
if [[ -n "${COMPANION_VERSION:-}" ]]; then
|
||||||
|
echo "Info: running acme-companion version ${COMPANION_VERSION}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function check_docker_socket {
|
||||||
|
if [[ $DOCKER_HOST == unix://* ]]; then
|
||||||
|
socket_file=${DOCKER_HOST#unix://}
|
||||||
|
if [[ ! -S $socket_file ]]; then
|
||||||
|
echo "Error: you need to share your Docker host socket with a volume at $socket_file" >&2
|
||||||
|
echo "Typically you should run your container with: '-v /var/run/docker.sock:$socket_file:ro'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function check_writable_directory {
|
||||||
|
local dir="$1"
|
||||||
|
if [[ $(get_self_cid) ]]; then
|
||||||
|
if ! docker_api "/containers/$(get_self_cid)/json" | jq ".Mounts[].Destination" | grep -q "^\"$dir\"$"; then
|
||||||
|
echo "Warning: '$dir' does not appear to be a mounted volume."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Warning: can't check if '$dir' is a mounted volume without self container ID."
|
||||||
|
fi
|
||||||
|
if [[ ! -d "$dir" ]]; then
|
||||||
|
echo "Error: can't access to '$dir' directory !" >&2
|
||||||
|
echo "Check that '$dir' directory is declared as a writable volume." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! touch "$dir/.check_writable" 2>/dev/null ; then
|
||||||
|
echo "Error: can't write to the '$dir' directory !" >&2
|
||||||
|
echo "Check that '$dir' directory is export as a writable volume." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
rm -f "$dir/.check_writable"
|
||||||
|
}
|
||||||
|
|
||||||
|
function check_dh_group {
|
||||||
|
# Credits to Steve Kamerman for the background Diffie-Hellman creation logic.
|
||||||
|
# https://github.com/nginx-proxy/nginx-proxy/pull/589
|
||||||
|
local DHPARAM_BITS="${DHPARAM_BITS:-2048}"
|
||||||
|
re='^[0-9]*$'
|
||||||
|
if ! [[ "$DHPARAM_BITS" =~ $re ]] ; then
|
||||||
|
echo "Error: invalid Diffie-Hellman size of $DHPARAM_BITS !" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If a dhparam file is not available, use the pre-generated one and generate a new one in the background.
|
||||||
|
local PREGEN_DHPARAM_FILE="/app/dhparam.pem.default"
|
||||||
|
local DHPARAM_FILE="/etc/nginx/certs/dhparam.pem"
|
||||||
|
local GEN_LOCKFILE="/tmp/le_companion_dhparam_generating.lock"
|
||||||
|
|
||||||
|
# The hash of the pregenerated dhparam file is used to check if the pregen dhparam is already in use
|
||||||
|
local PREGEN_HASH; PREGEN_HASH=$(sha256sum "$PREGEN_DHPARAM_FILE" | cut -d ' ' -f1)
|
||||||
|
if [[ -f "$DHPARAM_FILE" ]]; then
|
||||||
|
local CURRENT_HASH; CURRENT_HASH=$(sha256sum "$DHPARAM_FILE" | cut -d ' ' -f1)
|
||||||
|
if [[ "$PREGEN_HASH" != "$CURRENT_HASH" ]]; then
|
||||||
|
# There is already a dhparam, and it's not the default
|
||||||
|
set_ownership_and_permissions "$DHPARAM_FILE"
|
||||||
|
echo "Info: Custom Diffie-Hellman group found, generation skipped."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$GEN_LOCKFILE" ]]; then
|
||||||
|
# Generation is already in progress
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Info: Creating Diffie-Hellman group in the background."
|
||||||
|
echo "A pre-generated Diffie-Hellman group will be used for now while the new one is being created."
|
||||||
|
|
||||||
|
# Put the default dhparam file in place so we can start immediately
|
||||||
|
cp "$PREGEN_DHPARAM_FILE" "$DHPARAM_FILE"
|
||||||
|
set_ownership_and_permissions "$DHPARAM_FILE"
|
||||||
|
touch "$GEN_LOCKFILE"
|
||||||
|
|
||||||
|
# Generate a new dhparam in the background in a low priority and reload nginx when finished (grep removes the progress indicator).
|
||||||
|
(
|
||||||
|
(
|
||||||
|
nice -n +5 openssl dhparam -out "${DHPARAM_FILE}.new" "$DHPARAM_BITS" 2>&1 \
|
||||||
|
&& mv "${DHPARAM_FILE}.new" "$DHPARAM_FILE" \
|
||||||
|
&& echo "Info: Diffie-Hellman group creation complete, reloading nginx." \
|
||||||
|
&& set_ownership_and_permissions "$DHPARAM_FILE" \
|
||||||
|
&& reload_nginx
|
||||||
|
) | grep -vE '^[\.+]+'
|
||||||
|
rm "$GEN_LOCKFILE"
|
||||||
|
) & disown
|
||||||
|
}
|
||||||
|
|
||||||
|
function check_default_cert_key {
|
||||||
|
local cn='letsencrypt-nginx-proxy-companion'
|
||||||
|
|
||||||
|
if [[ -e /etc/nginx/certs/default.crt && -e /etc/nginx/certs/default.key ]]; then
|
||||||
|
default_cert_cn="$(openssl x509 -noout -subject -in /etc/nginx/certs/default.crt)"
|
||||||
|
# Check if the existing default certificate is still valid for more
|
||||||
|
# than 3 months / 7776000 seconds (60 x 60 x 24 x 30 x 3).
|
||||||
|
check_cert_min_validity /etc/nginx/certs/default.crt 7776000
|
||||||
|
cert_validity=$?
|
||||||
|
[[ "$DEBUG" == 1 ]] && echo "Debug: a default certificate with $default_cert_cn is present."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create a default cert and private key if:
|
||||||
|
# - either default.crt or default.key are absent
|
||||||
|
# OR
|
||||||
|
# - the existing default cert/key were generated by the container
|
||||||
|
# and the cert validity is less than three months
|
||||||
|
if [[ ! -e /etc/nginx/certs/default.crt || ! -e /etc/nginx/certs/default.key ]] || [[ "${default_cert_cn:-}" =~ $cn && "${cert_validity:-}" -ne 0 ]]; then
|
||||||
|
openssl req -x509 \
|
||||||
|
-newkey rsa:4096 -sha256 -nodes -days 365 \
|
||||||
|
-subj "/CN=$cn" \
|
||||||
|
-keyout /etc/nginx/certs/default.key.new \
|
||||||
|
-out /etc/nginx/certs/default.crt.new \
|
||||||
|
&& mv /etc/nginx/certs/default.key.new /etc/nginx/certs/default.key \
|
||||||
|
&& mv /etc/nginx/certs/default.crt.new /etc/nginx/certs/default.crt \
|
||||||
|
&& reload_nginx
|
||||||
|
echo "Info: a default key and certificate have been created at /etc/nginx/certs/default.key and /etc/nginx/certs/default.crt."
|
||||||
|
elif [[ "$DEBUG" == 1 && "${default_cert_cn:-}" =~ $cn ]]; then
|
||||||
|
echo "Debug: the self generated default certificate is still valid for more than three months. Skipping default certificate creation."
|
||||||
|
elif [[ "$DEBUG" == 1 ]]; then
|
||||||
|
echo "Debug: the default certificate is user provided. Skipping default certificate creation."
|
||||||
|
fi
|
||||||
|
set_ownership_and_permissions "/etc/nginx/certs/default.key"
|
||||||
|
set_ownership_and_permissions "/etc/nginx/certs/default.crt"
|
||||||
|
}
|
||||||
|
|
||||||
|
function check_default_account {
|
||||||
|
# The default account is now for empty account email
|
||||||
|
if [[ -f /etc/acme.sh/default/account.conf ]]; then
|
||||||
|
if grep -q ACCOUNT_EMAIL /etc/acme.sh/default/account.conf; then
|
||||||
|
sed -i '/ACCOUNT_EMAIL/d' /etc/acme.sh/default/account.conf
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$*" == "/bin/bash /app/start.sh" ]]; then
|
||||||
|
print_version
|
||||||
|
check_docker_socket
|
||||||
|
if [[ -z "$(get_nginx_proxy_container)" ]]; then
|
||||||
|
echo "Error: can't get nginx-proxy container ID !" >&2
|
||||||
|
echo "Check that you are doing one of the following :" >&2
|
||||||
|
echo -e "\t- Use the --volumes-from option to mount volumes from the nginx-proxy container." >&2
|
||||||
|
echo -e "\t- Set the NGINX_PROXY_CONTAINER env var on the letsencrypt-companion container to the name of the nginx-proxy container." >&2
|
||||||
|
echo -e "\t- Label the nginx-proxy container to use with 'com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy'." >&2
|
||||||
|
exit 1
|
||||||
|
elif [[ -z "$(get_docker_gen_container)" ]] && ! is_docker_gen_container "$(get_nginx_proxy_container)"; then
|
||||||
|
echo "Error: can't get docker-gen container id !" >&2
|
||||||
|
echo "If you are running a three containers setup, check that you are doing one of the following :" >&2
|
||||||
|
echo -e "\t- Set the NGINX_DOCKER_GEN_CONTAINER env var on the letsencrypt-companion container to the name of the docker-gen container." >&2
|
||||||
|
echo -e "\t- Label the docker-gen container to use with 'com.github.jrcs.letsencrypt_nginx_proxy_companion.docker_gen'." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
check_writable_directory '/etc/nginx/certs'
|
||||||
|
check_writable_directory '/etc/nginx/vhost.d'
|
||||||
|
check_writable_directory '/etc/acme.sh'
|
||||||
|
check_writable_directory '/usr/share/nginx/html'
|
||||||
|
[[ -f /app/letsencrypt_user_data ]] && check_writable_directory '/etc/nginx/conf.d'
|
||||||
|
check_default_cert_key
|
||||||
|
check_dh_group
|
||||||
|
reload_nginx
|
||||||
|
check_default_account
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
6
app/force_renew
Executable file
6
app/force_renew
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# shellcheck source=letsencrypt_service
|
||||||
|
source /app/letsencrypt_service --source-only
|
||||||
|
|
||||||
|
update_certs --force-renew
|
||||||
395
app/functions.sh
Normal file
395
app/functions.sh
Normal file
@ -0,0 +1,395 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Convert argument to lowercase (bash 4 only)
|
||||||
|
function lc {
|
||||||
|
echo "${@,,}"
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG="$(lc "${DEBUG:-}")"
|
||||||
|
if [[ "$DEBUG" == true ]]; then
|
||||||
|
DEBUG=1 && export DEBUG
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -z "${VHOST_DIR:-}" ]] && \
|
||||||
|
declare -r VHOST_DIR=/etc/nginx/vhost.d
|
||||||
|
[[ -z "${START_HEADER:-}" ]] && \
|
||||||
|
declare -r START_HEADER='## Start of configuration add by letsencrypt container'
|
||||||
|
[[ -z "${END_HEADER:-}" ]] && \
|
||||||
|
declare -r END_HEADER='## End of configuration add by letsencrypt container'
|
||||||
|
|
||||||
|
function check_nginx_proxy_container_run {
|
||||||
|
local _nginx_proxy_container; _nginx_proxy_container=$(get_nginx_proxy_container)
|
||||||
|
if [[ -n "$_nginx_proxy_container" ]]; then
|
||||||
|
if [[ $(docker_api "/containers/${_nginx_proxy_container}/json" | jq -r '.State.Status') = "running" ]];then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "$(date "+%Y/%m/%d %T") Error: nginx-proxy container ${_nginx_proxy_container} isn't running." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "$(date "+%Y/%m/%d %T") Error: could not get a nginx-proxy container ID." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function ascending_wildcard_locations {
|
||||||
|
# Given foo.bar.baz.example.com as argument, will output:
|
||||||
|
# - *.bar.baz.example.com
|
||||||
|
# - *.baz.example.com
|
||||||
|
# - *.example.com
|
||||||
|
local domain="${1:?}"
|
||||||
|
local first_label
|
||||||
|
regex="^[[:alnum:]_\-]+(\.[[:alpha:]]+)?$"
|
||||||
|
until [[ "$domain" =~ $regex ]]; do
|
||||||
|
first_label="${domain%%.*}"
|
||||||
|
domain="${domain/${first_label}./}"
|
||||||
|
echo "*.${domain}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
function descending_wildcard_locations {
|
||||||
|
# Given foo.bar.baz.example.com as argument, will output:
|
||||||
|
# - foo.bar.baz.example.*
|
||||||
|
# - foo.bar.baz.*
|
||||||
|
# - foo.bar.*
|
||||||
|
# - foo.*
|
||||||
|
local domain="${1:?}"
|
||||||
|
local last_label
|
||||||
|
regex="^[[:alnum:]_\-]+$"
|
||||||
|
until [[ "$domain" =~ $regex ]]; do
|
||||||
|
last_label="${domain##*.}"
|
||||||
|
domain="${domain/.${last_label}/}"
|
||||||
|
echo "${domain}.*"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
function enumerate_wildcard_locations {
|
||||||
|
# Goes through ascending then descending wildcard locations for a given FQDN
|
||||||
|
local domain="${1:?}"
|
||||||
|
ascending_wildcard_locations "$domain"
|
||||||
|
descending_wildcard_locations "$domain"
|
||||||
|
}
|
||||||
|
|
||||||
|
function add_location_configuration {
|
||||||
|
local domain="${1:-}"
|
||||||
|
local wildcard_domain
|
||||||
|
# If no domain was passed use default instead
|
||||||
|
[[ -z "$domain" ]] && domain='default'
|
||||||
|
|
||||||
|
# If the domain does not have an exact matching location file, test the possible
|
||||||
|
# wildcard locations files. Use default is no location file is present at all.
|
||||||
|
if [[ ! -f "${VHOST_DIR}/${domain}" ]]; then
|
||||||
|
while read -r wildcard_domain; do
|
||||||
|
if [[ -f "${VHOST_DIR}/${wildcard_domain}" ]]; then
|
||||||
|
domain="$wildcard_domain"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
domain='default'
|
||||||
|
done <<< "$(enumerate_wildcard_locations "$domain")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "${VHOST_DIR}/${domain}" && -n $(sed -n "/$START_HEADER/,/$END_HEADER/p" "${VHOST_DIR}/${domain}") ]]; then
|
||||||
|
# If the config file exist and already have the location configuration, end with exit code 0
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
# Else write the location configuration to a temp file ...
|
||||||
|
echo "$START_HEADER" > "${VHOST_DIR}/${domain}".new
|
||||||
|
cat /app/nginx_location.conf >> "${VHOST_DIR}/${domain}".new
|
||||||
|
echo "$END_HEADER" >> "${VHOST_DIR}/${domain}".new
|
||||||
|
# ... append the existing file content to the temp one ...
|
||||||
|
[[ -f "${VHOST_DIR}/${domain}" ]] && cat "${VHOST_DIR}/${domain}" >> "${VHOST_DIR}/${domain}".new
|
||||||
|
# ... and copy the temp file to the old one (if the destination file is bind mounted, you can't change
|
||||||
|
# its inode from within the container, so mv won't work and cp has to be used), then remove the temp file.
|
||||||
|
cp -f "${VHOST_DIR}/${domain}".new "${VHOST_DIR}/${domain}" && rm -f "${VHOST_DIR}/${domain}".new
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function add_standalone_configuration {
|
||||||
|
local domain="${1:?}"
|
||||||
|
if grep -q "server_name ${domain};" /etc/nginx/conf.d/*.conf; then
|
||||||
|
# If the domain is already present in nginx's conf, use the location configuration.
|
||||||
|
add_location_configuration "$domain"
|
||||||
|
else
|
||||||
|
# Else use the standalone configuration.
|
||||||
|
cat > "/etc/nginx/conf.d/standalone-cert-$domain.conf" << EOF
|
||||||
|
server {
|
||||||
|
server_name $domain;
|
||||||
|
listen 80;
|
||||||
|
access_log /var/log/nginx/access.log vhost;
|
||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
auth_basic off;
|
||||||
|
auth_request off;
|
||||||
|
allow all;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
try_files \$uri =404;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove_all_standalone_configurations {
|
||||||
|
local old_shopt_options; old_shopt_options=$(shopt -p) # Backup shopt options
|
||||||
|
shopt -s nullglob
|
||||||
|
for file in "/etc/nginx/conf.d/standalone-cert-"*".conf"; do
|
||||||
|
rm -f "$file"
|
||||||
|
done
|
||||||
|
eval "$old_shopt_options" # Restore shopt options
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove_all_location_configurations {
|
||||||
|
for file in "${VHOST_DIR}"/*; do
|
||||||
|
[[ -e "$file" ]] || continue
|
||||||
|
if [[ -n $(sed -n "/$START_HEADER/,/$END_HEADER/p" "$file") ]]; then
|
||||||
|
sed "/$START_HEADER/,/$END_HEADER/d" "$file" > "$file".new
|
||||||
|
cp -f "$file".new "$file" && rm -f "$file".new
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
function check_cert_min_validity {
|
||||||
|
# Check if a certificate ($1) is still valid for a given amount of time in seconds ($2).
|
||||||
|
# Returns 0 if the certificate is still valid for this amount of time, 1 otherwise.
|
||||||
|
local cert_path="$1"
|
||||||
|
local min_validity="$(( $(date "+%s") + $2 ))"
|
||||||
|
|
||||||
|
local cert_expiration
|
||||||
|
cert_expiration="$(openssl x509 -noout -enddate -in "$cert_path" | cut -d "=" -f 2)"
|
||||||
|
cert_expiration="$(date --utc --date "${cert_expiration% GMT}" "+%s")"
|
||||||
|
|
||||||
|
[[ $cert_expiration -gt $min_validity ]] || return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_self_cid {
|
||||||
|
local self_cid=""
|
||||||
|
|
||||||
|
# Try the /proc files methods first then resort to the Docker API.
|
||||||
|
if [[ -f /proc/1/cpuset ]]; then
|
||||||
|
self_cid="$(grep -Eo '[[:alnum:]]{64}' /proc/1/cpuset)"
|
||||||
|
fi
|
||||||
|
if [[ ( ${#self_cid} != 64 ) && ( -f /proc/self/cgroup ) ]]; then
|
||||||
|
self_cid="$(grep -Eo -m 1 '[[:alnum:]]{64}' /proc/self/cgroup)"
|
||||||
|
fi
|
||||||
|
if [[ ( ${#self_cid} != 64 ) ]]; then
|
||||||
|
self_cid="$(docker_api "/containers/$(hostname)/json" | jq -r '.Id')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If it's not 64 characters long, then it's probably not a container ID.
|
||||||
|
if [[ ${#self_cid} == 64 ]]; then
|
||||||
|
echo "$self_cid"
|
||||||
|
else
|
||||||
|
echo "$(date "+%Y/%m/%d %T"), Error: can't get my container ID !" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
## Docker API
|
||||||
|
function docker_api {
|
||||||
|
local scheme
|
||||||
|
local curl_opts=(-s)
|
||||||
|
local method=${2:-GET}
|
||||||
|
# data to POST
|
||||||
|
if [[ -n "${3:-}" ]]; then
|
||||||
|
curl_opts+=(-d "$3")
|
||||||
|
fi
|
||||||
|
if [[ -z "$DOCKER_HOST" ]];then
|
||||||
|
echo "Error DOCKER_HOST variable not set" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ $DOCKER_HOST == unix://* ]]; then
|
||||||
|
curl_opts+=(--unix-socket "${DOCKER_HOST#unix://}")
|
||||||
|
scheme='http://localhost'
|
||||||
|
else
|
||||||
|
scheme="http://${DOCKER_HOST#*://}"
|
||||||
|
fi
|
||||||
|
[[ $method = "POST" ]] && curl_opts+=(-H 'Content-Type: application/json')
|
||||||
|
curl "${curl_opts[@]}" -X "${method}" "${scheme}$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
function docker_exec {
|
||||||
|
local id="${1?missing id}"
|
||||||
|
local cmd="${2?missing command}"
|
||||||
|
local data; data=$(printf '{ "AttachStdin": false, "AttachStdout": true, "AttachStderr": true, "Tty":false,"Cmd": %s }' "$cmd")
|
||||||
|
exec_id=$(docker_api "/containers/$id/exec" "POST" "$data" | jq -r .Id)
|
||||||
|
if [[ -n "$exec_id" && "$exec_id" != "null" ]]; then
|
||||||
|
docker_api "/exec/${exec_id}/start" "POST" '{"Detach": false, "Tty":false}'
|
||||||
|
else
|
||||||
|
echo "$(date "+%Y/%m/%d %T"), Error: can't exec command ${cmd} in container ${id}. Check if the container is running." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function docker_restart {
|
||||||
|
local id="${1?missing id}"
|
||||||
|
docker_api "/containers/$id/restart" "POST"
|
||||||
|
}
|
||||||
|
|
||||||
|
function docker_kill {
|
||||||
|
local id="${1?missing id}"
|
||||||
|
local signal="${2?missing signal}"
|
||||||
|
docker_api "/containers/$id/kill?signal=$signal" "POST"
|
||||||
|
}
|
||||||
|
|
||||||
|
function labeled_cid {
|
||||||
|
docker_api "/containers/json" | jq -r '.[] | select(.Labels["'"$1"'"])|.Id'
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_docker_gen_container {
|
||||||
|
local id="${1?missing id}"
|
||||||
|
if [[ $(docker_api "/containers/$id/json" | jq -r '.Config.Env[]' | grep -c -E '^DOCKER_GEN_VERSION=') = "1" ]]; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_docker_gen_container {
|
||||||
|
# First try to get the docker-gen container ID from the container label.
|
||||||
|
local docker_gen_cid; docker_gen_cid="$(labeled_cid com.github.jrcs.letsencrypt_nginx_proxy_companion.docker_gen)"
|
||||||
|
|
||||||
|
# If the labeled_cid function dit not return anything and the env var is set, use it.
|
||||||
|
if [[ -z "$docker_gen_cid" ]] && [[ -n "${NGINX_DOCKER_GEN_CONTAINER:-}" ]]; then
|
||||||
|
docker_gen_cid="$NGINX_DOCKER_GEN_CONTAINER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If a container ID was found, output it. The function will return 1 otherwise.
|
||||||
|
[[ -n "$docker_gen_cid" ]] && echo "$docker_gen_cid"
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_nginx_proxy_container {
|
||||||
|
local volumes_from
|
||||||
|
# First try to get the nginx container ID from the container label.
|
||||||
|
local nginx_cid; nginx_cid="$(labeled_cid com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy)"
|
||||||
|
|
||||||
|
# If the labeled_cid function dit not return anything ...
|
||||||
|
if [[ -z "${nginx_cid}" ]]; then
|
||||||
|
# ... and the env var is set, use it ...
|
||||||
|
if [[ -n "${NGINX_PROXY_CONTAINER:-}" ]]; then
|
||||||
|
nginx_cid="$NGINX_PROXY_CONTAINER"
|
||||||
|
# ... else try to get the container ID with the volumes_from method.
|
||||||
|
elif [[ $(get_self_cid) ]]; then
|
||||||
|
volumes_from=$(docker_api "/containers/$(get_self_cid)/json" | jq -r '.HostConfig.VolumesFrom[]' 2>/dev/null)
|
||||||
|
for cid in $volumes_from; do
|
||||||
|
cid="${cid%:*}" # Remove leading :ro or :rw set by remote docker-compose (thx anoopr)
|
||||||
|
if [[ $(docker_api "/containers/$cid/json" | jq -r '.Config.Env[]' | grep -c -E '^NGINX_VERSION=') = "1" ]];then
|
||||||
|
nginx_cid="$cid"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If a container ID was found, output it. The function will return 1 otherwise.
|
||||||
|
[[ -n "$nginx_cid" ]] && echo "$nginx_cid"
|
||||||
|
}
|
||||||
|
|
||||||
|
## Nginx
|
||||||
|
function reload_nginx {
|
||||||
|
local _docker_gen_container; _docker_gen_container=$(get_docker_gen_container)
|
||||||
|
local _nginx_proxy_container; _nginx_proxy_container=$(get_nginx_proxy_container)
|
||||||
|
|
||||||
|
if [[ -n "${_docker_gen_container:-}" ]]; then
|
||||||
|
# Using docker-gen and nginx in separate container
|
||||||
|
echo "Reloading nginx docker-gen (using separate container ${_docker_gen_container})..."
|
||||||
|
docker_kill "${_docker_gen_container}" SIGHUP
|
||||||
|
|
||||||
|
if [[ -n "${_nginx_proxy_container:-}" ]]; then
|
||||||
|
# Reloading nginx in case only certificates had been renewed
|
||||||
|
echo "Reloading nginx (using separate container ${_nginx_proxy_container})..."
|
||||||
|
docker_kill "${_nginx_proxy_container}" SIGHUP
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [[ -n "${_nginx_proxy_container:-}" ]]; then
|
||||||
|
echo "Reloading nginx proxy (${_nginx_proxy_container})..."
|
||||||
|
docker_exec "${_nginx_proxy_container}" \
|
||||||
|
'[ "sh", "-c", "/app/docker-entrypoint.sh /usr/local/bin/docker-gen /app/nginx.tmpl /etc/nginx/conf.d/default.conf; /usr/sbin/nginx -s reload" ]' \
|
||||||
|
| sed -rn 's/^.*([0-9]{4}\/[0-9]{2}\/[0-9]{2}.*$)/\1/p'
|
||||||
|
[[ ${PIPESTATUS[0]} -eq 1 ]] && echo "$(date "+%Y/%m/%d %T"), Error: can't reload nginx-proxy." >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function set_ownership_and_permissions {
|
||||||
|
local path="${1:?}"
|
||||||
|
# The default ownership is root:root, with 755 permissions for folders and 644 for files.
|
||||||
|
local user="${FILES_UID:-root}"
|
||||||
|
local group="${FILES_GID:-$user}"
|
||||||
|
local f_perms="${FILES_PERMS:-644}"
|
||||||
|
local d_perms="${FOLDERS_PERMS:-755}"
|
||||||
|
|
||||||
|
if [[ ! "$f_perms" =~ ^[0-7]{3,4}$ ]]; then
|
||||||
|
echo "Warning : the provided files permission octal ($f_perms) is incorrect. Skipping ownership and permissions check."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ ! "$d_perms" =~ ^[0-7]{3,4}$ ]]; then
|
||||||
|
echo "Warning : the provided folders permission octal ($d_perms) is incorrect. Skipping ownership and permissions check."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ "$DEBUG" == 1 ]] && echo "Debug: checking $path ownership and permissions."
|
||||||
|
|
||||||
|
# Find the user numeric ID if the FILES_UID environment variable isn't numeric.
|
||||||
|
if [[ "$user" =~ ^[0-9]+$ ]]; then
|
||||||
|
user_num="$user"
|
||||||
|
# Check if this user exist inside the container
|
||||||
|
elif id -u "$user" > /dev/null 2>&1; then
|
||||||
|
# Convert the user name to numeric ID
|
||||||
|
local user_num; user_num="$(id -u "$user")"
|
||||||
|
[[ "$DEBUG" == 1 ]] && echo "Debug: numeric ID of user $user is $user_num."
|
||||||
|
else
|
||||||
|
echo "Warning: user $user not found in the container, please use a numeric user ID instead of a user name. Skipping ownership and permissions check."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find the group numeric ID if the FILES_GID environment variable isn't numeric.
|
||||||
|
if [[ "$group" =~ ^[0-9]+$ ]]; then
|
||||||
|
group_num="$group"
|
||||||
|
# Check if this group exist inside the container
|
||||||
|
elif getent group "$group" > /dev/null 2>&1; then
|
||||||
|
# Convert the group name to numeric ID
|
||||||
|
local group_num; group_num="$(getent group "$group" | awk -F ':' '{print $3}')"
|
||||||
|
[[ "$DEBUG" == 1 ]] && echo "Debug: numeric ID of group $group is $group_num."
|
||||||
|
else
|
||||||
|
echo "Warning: group $group not found in the container, please use a numeric group ID instead of a group name. Skipping ownership and permissions check."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check and modify ownership if required.
|
||||||
|
if [[ -e "$path" ]]; then
|
||||||
|
if [[ "$(stat -c %u:%g "$path" )" != "$user_num:$group_num" ]]; then
|
||||||
|
[[ "$DEBUG" == 1 ]] && echo "Debug: setting $path ownership to $user:$group."
|
||||||
|
if [[ -L "$path" ]]; then
|
||||||
|
chown -h "$user_num:$group_num" "$path"
|
||||||
|
else
|
||||||
|
chown "$user_num:$group_num" "$path"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# If the path is a folder, check and modify permissions if required.
|
||||||
|
if [[ -d "$path" ]]; then
|
||||||
|
if [[ "$(stat -c %a "$path")" != "$d_perms" ]]; then
|
||||||
|
[[ "$DEBUG" == 1 ]] && echo "Debug: setting $path permissions to $d_perms."
|
||||||
|
chmod "$d_perms" "$path"
|
||||||
|
fi
|
||||||
|
# If the path is a file, check and modify permissions if required.
|
||||||
|
elif [[ -f "$path" ]]; then
|
||||||
|
# Use different permissions for private files (private keys and ACME account files) ...
|
||||||
|
if [[ "$path" =~ ^.*(default\.key|key\.pem|\.json)$ ]]; then
|
||||||
|
if [[ "$(stat -c %a "$path")" != "$f_perms" ]]; then
|
||||||
|
[[ "$DEBUG" == 1 ]] && echo "Debug: setting $path permissions to $f_perms."
|
||||||
|
chmod "$f_perms" "$path"
|
||||||
|
fi
|
||||||
|
# ... and for public files (certificates, chains, fullchains, DH parameters).
|
||||||
|
else
|
||||||
|
if [[ "$(stat -c %a "$path")" != "644" ]]; then
|
||||||
|
[[ "$DEBUG" == 1 ]] && echo "Debug: setting $path permissions to 644."
|
||||||
|
chmod "644" "$path"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Warning: $path does not exist. Skipping ownership and permissions check."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
424
app/letsencrypt_service
Executable file
424
app/letsencrypt_service
Executable file
@ -0,0 +1,424 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# shellcheck source=functions.sh
|
||||||
|
source /app/functions.sh
|
||||||
|
|
||||||
|
CERTS_UPDATE_INTERVAL="${CERTS_UPDATE_INTERVAL:-3600}"
|
||||||
|
ACME_CA_URI="${ACME_CA_URI:-"https://acme-v02.api.letsencrypt.org/directory"}"
|
||||||
|
ACME_CA_TEST_URI="https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||||
|
DEFAULT_KEY_SIZE="${DEFAULT_KEY_SIZE:-4096}"
|
||||||
|
RENEW_PRIVATE_KEYS="$(lc "${RENEW_PRIVATE_KEYS:-true}")"
|
||||||
|
|
||||||
|
# Backward compatibility environment variable
|
||||||
|
REUSE_PRIVATE_KEYS="$(lc "${REUSE_PRIVATE_KEYS:-false}")"
|
||||||
|
|
||||||
|
function create_link {
|
||||||
|
local -r source=${1?missing source argument}
|
||||||
|
local -r target=${2?missing target argument}
|
||||||
|
if [[ -f "$target" ]] && [[ "$(readlink "$target")" == "$source" ]]; then
|
||||||
|
set_ownership_and_permissions "$target"
|
||||||
|
[[ "$DEBUG" == 1 ]] && echo "$target already linked to $source"
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
ln -sf "$source" "$target" \
|
||||||
|
&& set_ownership_and_permissions "$target"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function create_links {
|
||||||
|
local -r base_domain=${1?missing base_domain argument}
|
||||||
|
local -r domain=${2?missing base_domain argument}
|
||||||
|
|
||||||
|
if [[ ! -f "/etc/nginx/certs/$base_domain/fullchain.pem" || \
|
||||||
|
! -f "/etc/nginx/certs/$base_domain/key.pem" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local return_code=1
|
||||||
|
create_link "./$base_domain/fullchain.pem" "/etc/nginx/certs/$domain.crt"
|
||||||
|
return_code=$(( return_code & $? ))
|
||||||
|
create_link "./$base_domain/key.pem" "/etc/nginx/certs/$domain.key"
|
||||||
|
return_code=$(( return_code & $? ))
|
||||||
|
if [[ -f "/etc/nginx/certs/dhparam.pem" ]]; then
|
||||||
|
create_link ./dhparam.pem "/etc/nginx/certs/$domain.dhparam.pem"
|
||||||
|
return_code=$(( return_code & $? ))
|
||||||
|
fi
|
||||||
|
if [[ -f "/etc/nginx/certs/$base_domain/chain.pem" ]]; then
|
||||||
|
create_link "./$base_domain/chain.pem" "/etc/nginx/certs/$domain.chain.pem"
|
||||||
|
return_code=$(( return_code & $? ))
|
||||||
|
fi
|
||||||
|
return $return_code
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup_links {
|
||||||
|
local -a LETSENCRYPT_CONTAINERS
|
||||||
|
local -a LETSENCRYPT_STANDALONE_CERTS
|
||||||
|
local -a ENABLED_DOMAINS
|
||||||
|
local -a SYMLINKED_DOMAINS
|
||||||
|
local -a DISABLED_DOMAINS
|
||||||
|
|
||||||
|
# Create an array containing domains for which a
|
||||||
|
# symlinked private key exists in /etc/nginx/certs.
|
||||||
|
for symlinked_domain in /etc/nginx/certs/*.crt; do
|
||||||
|
[[ -L "$symlinked_domain" ]] || continue
|
||||||
|
symlinked_domain="${symlinked_domain##*/}"
|
||||||
|
symlinked_domain="${symlinked_domain%*.crt}"
|
||||||
|
SYMLINKED_DOMAINS+=("$symlinked_domain")
|
||||||
|
done
|
||||||
|
[[ "$DEBUG" == 1 ]] && echo "Symlinked domains: ${SYMLINKED_DOMAINS[*]}"
|
||||||
|
|
||||||
|
# Create an array containing domains that are considered
|
||||||
|
# enabled (ie present on /app/letsencrypt_service_data or /app/letsencrypt_user_data).
|
||||||
|
[[ -f /app/letsencrypt_service_data ]] && source /app/letsencrypt_service_data
|
||||||
|
[[ -f /app/letsencrypt_user_data ]] && source /app/letsencrypt_user_data
|
||||||
|
LETSENCRYPT_CONTAINERS+=( "${LETSENCRYPT_STANDALONE_CERTS[@]}" )
|
||||||
|
for cid in "${LETSENCRYPT_CONTAINERS[@]}"; do
|
||||||
|
local -n hosts_array="LETSENCRYPT_${cid}_HOST"
|
||||||
|
for domain in "${hosts_array[@]}"; do
|
||||||
|
# Add domain to the array storing currently enabled domains.
|
||||||
|
ENABLED_DOMAINS+=("$domain")
|
||||||
|
done
|
||||||
|
done
|
||||||
|
[[ "$DEBUG" == 1 ]] && echo "Enabled domains: ${ENABLED_DOMAINS[*]}"
|
||||||
|
|
||||||
|
# Create an array containing only domains for which a symlinked private key exists
|
||||||
|
# in /etc/nginx/certs but that no longer have a corresponding LETSENCRYPT_HOST set
|
||||||
|
# on an active container or on /app/letsencrypt_user_data
|
||||||
|
if [[ ${#SYMLINKED_DOMAINS[@]} -gt 0 ]]; then
|
||||||
|
mapfile -t DISABLED_DOMAINS < <(echo "${SYMLINKED_DOMAINS[@]}" \
|
||||||
|
"${ENABLED_DOMAINS[@]}" \
|
||||||
|
"${ENABLED_DOMAINS[@]}" \
|
||||||
|
| tr ' ' '\n' | sort | uniq -u)
|
||||||
|
fi
|
||||||
|
[[ "$DEBUG" == 1 ]] && echo "Disabled domains: ${DISABLED_DOMAINS[*]}"
|
||||||
|
|
||||||
|
|
||||||
|
# Remove disabled domains symlinks if present.
|
||||||
|
# Return 1 if nothing was removed and 0 otherwise.
|
||||||
|
if [[ ${#DISABLED_DOMAINS[@]} -gt 0 ]]; then
|
||||||
|
[[ "$DEBUG" == 1 ]] && echo "Some domains are disabled :"
|
||||||
|
for disabled_domain in "${DISABLED_DOMAINS[@]}"; do
|
||||||
|
[[ "$DEBUG" == 1 ]] && echo "Checking domain ${disabled_domain}"
|
||||||
|
cert_folder="$(readlink -f "/etc/nginx/certs/${disabled_domain}.crt")"
|
||||||
|
# If the dotfile is absent, skip domain.
|
||||||
|
if [[ ! -e "${cert_folder%/*}/.companion" ]]; then
|
||||||
|
[[ "$DEBUG" == 1 ]] && echo "No .companion file found in ${cert_folder}. ${disabled_domain} is not managed by acme-companion. Skipping domain."
|
||||||
|
continue
|
||||||
|
else
|
||||||
|
[[ "$DEBUG" == 1 ]] && echo "${disabled_domain} is managed by acme-companion. Removing unused symlinks."
|
||||||
|
fi
|
||||||
|
|
||||||
|
for extension in .crt .key .dhparam.pem .chain.pem; do
|
||||||
|
file="${disabled_domain}${extension}"
|
||||||
|
if [[ -n "${file// }" ]] && [[ -L "/etc/nginx/certs/${file}" ]]; then
|
||||||
|
[[ "$DEBUG" == 1 ]] && echo "Removing /etc/nginx/certs/${file}"
|
||||||
|
rm -f "/etc/nginx/certs/${file}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_cert {
|
||||||
|
local cid="${1:?}"
|
||||||
|
local -n hosts_array="LETSENCRYPT_${cid}_HOST"
|
||||||
|
# First domain will be our base domain
|
||||||
|
local base_domain="${hosts_array[0]}"
|
||||||
|
|
||||||
|
local should_restart_container='false'
|
||||||
|
|
||||||
|
# Base CLI parameters array, used for both --register-account and --issue
|
||||||
|
local -a params_base_arr
|
||||||
|
params_base_arr+=(--log /dev/null)
|
||||||
|
[[ "$DEBUG" == 1 ]] && params_base_arr+=(--debug 2)
|
||||||
|
|
||||||
|
# Alternative trusted root CA path, used for test with Pebble
|
||||||
|
if [[ -n "${CA_BUNDLE// }" ]]; then
|
||||||
|
if [[ -f "$CA_BUNDLE" ]]; then
|
||||||
|
params_base_arr+=(--ca-bundle "$CA_BUNDLE")
|
||||||
|
[[ "$DEBUG" == 1 ]] && echo "Debug: acme.sh will use $CA_BUNDLE as trusted root CA."
|
||||||
|
else
|
||||||
|
echo "Warning: the path to the alternate CA bundle ($CA_BUNDLE) is not valid, using default Alpine trust store."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# CLI parameters array used for --register-account
|
||||||
|
local -a params_register_arr
|
||||||
|
|
||||||
|
# CLI parameters array used for --issue
|
||||||
|
local -a params_issue_arr
|
||||||
|
params_issue_arr+=(--webroot /usr/share/nginx/html)
|
||||||
|
|
||||||
|
local -n cert_keysize="LETSENCRYPT_${cid}_KEYSIZE"
|
||||||
|
if [[ -z "$cert_keysize" || "$cert_keysize" == "<no value>" ]] || \
|
||||||
|
[[ ! "$cert_keysize" =~ ^(2048|3072|4096|ec-256|ec-384)$ ]]; then
|
||||||
|
cert_keysize=$DEFAULT_KEY_SIZE
|
||||||
|
fi
|
||||||
|
params_issue_arr+=(--keylength "$cert_keysize")
|
||||||
|
|
||||||
|
# OCSP-Must-Staple extension
|
||||||
|
local -n ocsp="ACME_${cid}_OCSP"
|
||||||
|
if [[ $(lc "$ocsp") == true ]]; then
|
||||||
|
params_issue_arr+=(--ocsp-must-staple)
|
||||||
|
fi
|
||||||
|
|
||||||
|
local -n accountemail="LETSENCRYPT_${cid}_EMAIL"
|
||||||
|
local config_home
|
||||||
|
# If we don't have a LETSENCRYPT_EMAIL from the proxied container
|
||||||
|
# and DEFAULT_EMAIL is set to a non empty value, use the latter.
|
||||||
|
if [[ -z "$accountemail" || "$accountemail" == "<no value>" ]]; then
|
||||||
|
if [[ -n "${DEFAULT_EMAIL// }" ]]; then
|
||||||
|
accountemail="$DEFAULT_EMAIL"
|
||||||
|
else
|
||||||
|
unset accountemail
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ -n "${accountemail// }" ]]; then
|
||||||
|
# If we got an email, use it with the corresponding config home
|
||||||
|
config_home="/etc/acme.sh/$accountemail"
|
||||||
|
else
|
||||||
|
# If we did not get any email at all, use the default (empty mail) config
|
||||||
|
config_home="/etc/acme.sh/default"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local -n acme_ca_uri="ACME_${cid}_CA_URI"
|
||||||
|
if [[ -z "$acme_ca_uri" || "$acme_ca_uri" == "<no value>" ]]; then
|
||||||
|
# Use default or user provided ACME end point
|
||||||
|
acme_ca_uri="$ACME_CA_URI"
|
||||||
|
fi
|
||||||
|
# LETSENCRYPT_TEST overrides LETSENCRYPT_ACME_CA_URI
|
||||||
|
local -n test_certificate="LETSENCRYPT_${cid}_TEST"
|
||||||
|
if [[ $(lc "$test_certificate") == true ]]; then
|
||||||
|
# Use Let's Encrypt ACME V2 staging end point
|
||||||
|
acme_ca_uri="$ACME_CA_TEST_URI"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set relevant --server parameter and ca folder name
|
||||||
|
params_base_arr+=(--server "$acme_ca_uri")
|
||||||
|
local ca_dir="${acme_ca_uri##*://}" \
|
||||||
|
&& ca_dir="${ca_dir%%/*}" \
|
||||||
|
&& ca_dir="${ca_dir%%:*}"
|
||||||
|
|
||||||
|
local certificate_dir
|
||||||
|
# If we're going to use one of LE stating endpoints ...
|
||||||
|
if [[ "$acme_ca_uri" =~ ^https://acme-staging.* ]]; then
|
||||||
|
# Unset accountemail
|
||||||
|
# force config dir to 'staging'
|
||||||
|
unset accountemail
|
||||||
|
config_home="/etc/acme.sh/staging"
|
||||||
|
# Prefix test certificate directory with _test_
|
||||||
|
certificate_dir="/etc/nginx/certs/_test_$base_domain"
|
||||||
|
else
|
||||||
|
certificate_dir="/etc/nginx/certs/$base_domain"
|
||||||
|
fi
|
||||||
|
params_issue_arr+=( \
|
||||||
|
--cert-file "${certificate_dir}/cert.pem" \
|
||||||
|
--key-file "${certificate_dir}/key.pem" \
|
||||||
|
--ca-file "${certificate_dir}/chain.pem" \
|
||||||
|
--fullchain-file "${certificate_dir}/fullchain.pem" \
|
||||||
|
)
|
||||||
|
|
||||||
|
[[ ! -d "$config_home" ]] && mkdir -p "$config_home"
|
||||||
|
params_base_arr+=(--config-home "$config_home")
|
||||||
|
local account_file="${config_home}/ca/${ca_dir}/account.json"
|
||||||
|
|
||||||
|
# Zero SSL External Account Binding (EAB)
|
||||||
|
if [[ "$acme_ca_uri" == "https://acme.zerossl.com/v2/DV90" ]]; then
|
||||||
|
local -n eab_kid="ACME_${cid}_EAB_KID"
|
||||||
|
local -n eab_hmac_key="ACME_${cid}_EAB_HMAC_KEY"
|
||||||
|
local -n zerossl_api_key="ZEROSSL_${cid}_API_KEY"
|
||||||
|
if [[ -z "$zerossl_api_key" || "$zerossl_api_key" == "<no value>" ]]; then
|
||||||
|
# Try using the default API key
|
||||||
|
zerossl_api_key="$ZEROSSL_API_KEY"
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$account_file" ]]; then
|
||||||
|
if [[ -n "${eab_kid// }" && "$eab_kid" != "<no value>" && -n "${eab_hmac_key// }" && "$eab_hmac_key" != "<no value>" ]]; then
|
||||||
|
# Register the ACME account with the per container EAB credentials.
|
||||||
|
params_register_arr+=(--eab-kid "$eab_kid" --eab-hmac-key "$eab_hmac_key")
|
||||||
|
elif [[ -n "${zerossl_api_key// }" && "$zerossl_api_key" != "<no value>" ]]; then
|
||||||
|
# We have a Zero SSL API key but no per-container EAB kid and hmac key.
|
||||||
|
# Generate a set of ACME EAB credentials using the ZeroSSL API.
|
||||||
|
local zerossl_api_response
|
||||||
|
if zerossl_api_response="$(curl -s -X POST "https://api.zerossl.com/acme/eab-credentials?access_key=${zerossl_api_key}")"; then
|
||||||
|
if [[ "$(jq -r .success <<< "$zerossl_api_response")" == 'true' ]]; then
|
||||||
|
eab_kid="$(jq -r .eab_kid <<< "$zerossl_api_response")"
|
||||||
|
eab_hmac_key="$(jq -r .eab_hmac_key <<< "$zerossl_api_response")"
|
||||||
|
params_register_arr+=(--eab-kid "$eab_kid" --eab-hmac-key "$eab_hmac_key")
|
||||||
|
[[ "$DEBUG" == 1 ]] && echo "Successfull EAB credentials request against the ZeroSSL API, got the following EAB kid : ${eab_kid}"
|
||||||
|
else
|
||||||
|
# The JSON response body indicated an unsuccesfull API call.
|
||||||
|
echo "Warning: the EAB credentials request against the ZeroSSL API was not successfull."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# curl failed.
|
||||||
|
echo "Warning: curl failed to make an HTTP POST request to https://api.zerossl.com/acme/eab-credentials."
|
||||||
|
fi
|
||||||
|
elif [[ -n "${ACME_EAB_KID// }" && -n "${ACME_EAB_HMAC_KEY// }" ]]; then
|
||||||
|
# We don't have per-container EAB kid and hmac key or Zero SSL API key.
|
||||||
|
# Register the ACME account with the default EAB credentials.
|
||||||
|
params_register_arr+=(--eab-kid "$ACME_EAB_KID" --eab-hmac-key "$ACME_EAB_HMAC_KEY")
|
||||||
|
elif [[ -n "${accountemail// }" ]]; then
|
||||||
|
# We don't have per container nor default EAB credentials, register a new account with ZeroSSL.
|
||||||
|
params_register_arr+=(--accountemail "$accountemail")
|
||||||
|
else
|
||||||
|
# We don't have a Zero SSL ACME account, EAB credentials, a ZeroSSL API key or an account email :
|
||||||
|
# skip certificate account registration and certificate issuance.
|
||||||
|
echo "Error: usage of ZeroSSL require an email bound account. No EAB credentials, ZeroSSL API key or email were provided for this certificate, creation aborted."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif [[ -n "${accountemail// }" ]]; then
|
||||||
|
# We're not using Zero SSL, register the ACME account using the provided email.
|
||||||
|
params_register_arr+=(--accountemail "$accountemail")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Account registration and update if required
|
||||||
|
if [[ ! -f "$account_file" ]]; then
|
||||||
|
params_register_arr=("${params_base_arr[@]}" "${params_register_arr[@]}")
|
||||||
|
[[ "$DEBUG" == 1 ]] && echo "Calling acme.sh --register-account with the following parameters : ${params_register_arr[*]}"
|
||||||
|
acme.sh --register-account "${params_register_arr[@]}"
|
||||||
|
fi
|
||||||
|
if [[ -n "${accountemail// }" ]] && ! grep -q "mailto:$accountemail" "$account_file"; then
|
||||||
|
local -a params_update_arr=("${params_base_arr[@]}" --accountemail "$accountemail")
|
||||||
|
[[ "$DEBUG" == 1 ]] && echo "Calling acme.sh --update-account with the following parameters : ${params_update_arr[*]}"
|
||||||
|
acme.sh --update-account "${params_update_arr[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If we still don't have an account.json file by this point, we've got an issue
|
||||||
|
if [[ ! -f "$account_file" ]]; then
|
||||||
|
echo "Error: no ACME account was found or registered for $accountemail and $acme_ca_uri, certificate creation aborted."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local -n acme_preferred_chain="ACME_${cid}_PREFERRED_CHAIN"
|
||||||
|
if [[ -n "${acme_preferred_chain// }" && "$acme_preferred_chain" != "<no value>" ]]; then
|
||||||
|
# Using amce.sh --preferred-chain to select alternate chain.
|
||||||
|
params_issue_arr+=(--preferred-chain "$acme_preferred_chain")
|
||||||
|
fi
|
||||||
|
if [[ "$RENEW_PRIVATE_KEYS" != 'false' && "$REUSE_PRIVATE_KEYS" != 'true' ]]; then
|
||||||
|
params_issue_arr+=(--always-force-new-domain-key)
|
||||||
|
fi
|
||||||
|
[[ "${2:-}" == "--force-renew" ]] && params_issue_arr+=(--force)
|
||||||
|
|
||||||
|
# Create directory for the first domain
|
||||||
|
mkdir -p "$certificate_dir"
|
||||||
|
set_ownership_and_permissions "$certificate_dir"
|
||||||
|
|
||||||
|
for domain in "${hosts_array[@]}"; do
|
||||||
|
# Add all the domains to certificate
|
||||||
|
params_issue_arr+=(--domain "$domain")
|
||||||
|
# Add location configuration for the domain
|
||||||
|
add_location_configuration "$domain" || reload_nginx
|
||||||
|
done
|
||||||
|
|
||||||
|
params_issue_arr=("${params_base_arr[@]}" "${params_issue_arr[@]}")
|
||||||
|
[[ "$DEBUG" == 1 ]] && echo "Calling acme.sh --issue with the following parameters : ${params_issue_arr[*]}"
|
||||||
|
echo "Creating/renewal $base_domain certificates... (${hosts_array[*]})"
|
||||||
|
acme.sh --issue "${params_issue_arr[@]}"
|
||||||
|
|
||||||
|
local acmesh_return=$?
|
||||||
|
|
||||||
|
# 0 = success, 2 = RENEW_SKIP
|
||||||
|
if [[ $acmesh_return == 0 || $acmesh_return == 2 ]]; then
|
||||||
|
for domain in "${hosts_array[@]}"; do
|
||||||
|
if [[ $acme_ca_uri =~ ^https://acme-staging.* ]]; then
|
||||||
|
create_links "_test_$base_domain" "$domain" \
|
||||||
|
&& should_reload_nginx='true' \
|
||||||
|
&& should_restart_container='true'
|
||||||
|
else
|
||||||
|
create_links "$base_domain" "$domain" \
|
||||||
|
&& should_reload_nginx='true' \
|
||||||
|
&& should_restart_container='true'
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "${COMPANION_VERSION:-}" > "${certificate_dir}/.companion"
|
||||||
|
set_ownership_and_permissions "${certificate_dir}/.companion"
|
||||||
|
# Make private key root readable only
|
||||||
|
for file in cert.pem key.pem chain.pem fullchain.pem; do
|
||||||
|
local file_path="${certificate_dir}/${file}"
|
||||||
|
[[ -e "$file_path" ]] && set_ownership_and_permissions "$file_path"
|
||||||
|
done
|
||||||
|
# Queue nginx reload if a certificate was issued or renewed
|
||||||
|
[[ $acmesh_return -eq 0 ]] \
|
||||||
|
&& should_reload_nginx='true' \
|
||||||
|
&& should_restart_container='true'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Restart container if certs are updated and the respective environmental variable is set
|
||||||
|
local -n restart_container="LETSENCRYPT_${cid}_RESTART_CONTAINER"
|
||||||
|
if [[ $(lc "$restart_container") == true ]] && [[ "$should_restart_container" == 'true' ]]; then
|
||||||
|
echo "Restarting container (${cid})..."
|
||||||
|
docker_restart "${cid}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for domain in "${hosts_array[@]}"; do
|
||||||
|
if [[ -f "/etc/nginx/conf.d/standalone-cert-$domain.conf" ]]; then
|
||||||
|
[[ "$DEBUG" == 1 ]] && echo "Debug: removing standalone configuration file /etc/nginx/conf.d/standalone-cert-$domain.conf"
|
||||||
|
rm -f "/etc/nginx/conf.d/standalone-cert-$domain.conf" && should_reload_nginx='true'
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_certs {
|
||||||
|
local -a LETSENCRYPT_CONTAINERS
|
||||||
|
local -a LETSENCRYPT_STANDALONE_CERTS
|
||||||
|
|
||||||
|
pushd /etc/nginx/certs > /dev/null || return
|
||||||
|
check_nginx_proxy_container_run || return
|
||||||
|
|
||||||
|
# Load relevant container settings
|
||||||
|
if [[ -f /app/letsencrypt_service_data ]]; then
|
||||||
|
source /app/letsencrypt_service_data
|
||||||
|
else
|
||||||
|
echo "Warning: /app/letsencrypt_service_data not found, skipping data from containers."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Load settings for standalone certs
|
||||||
|
if [[ -f /app/letsencrypt_user_data ]]; then
|
||||||
|
if source /app/letsencrypt_user_data; then
|
||||||
|
for cid in "${LETSENCRYPT_STANDALONE_CERTS[@]}"; do
|
||||||
|
local -n hosts_array="LETSENCRYPT_${cid}_HOST"
|
||||||
|
for domain in "${hosts_array[@]}"; do
|
||||||
|
add_standalone_configuration "$domain"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
reload_nginx
|
||||||
|
LETSENCRYPT_CONTAINERS+=( "${LETSENCRYPT_STANDALONE_CERTS[@]}" )
|
||||||
|
else
|
||||||
|
echo "Warning: could not source /app/letsencrypt_user_data, skipping user data"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
should_reload_nginx='false'
|
||||||
|
for cid in "${LETSENCRYPT_CONTAINERS[@]}"; do
|
||||||
|
# Pass the eventual --force-renew arg to update_cert() as second arg
|
||||||
|
update_cert "$cid" "${1:-}"
|
||||||
|
done
|
||||||
|
|
||||||
|
cleanup_links && should_reload_nginx='true'
|
||||||
|
|
||||||
|
[[ "$should_reload_nginx" == 'true' ]] && reload_nginx
|
||||||
|
|
||||||
|
popd > /dev/null || return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Allow the script functions to be sourced without starting the Service Loop.
|
||||||
|
if [ "${1}" == "--source-only" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
pid=
|
||||||
|
# Service Loop: When this script exits, start it again.
|
||||||
|
trap '[[ $pid ]] && kill $pid; exec $0' EXIT
|
||||||
|
trap 'trap - EXIT' INT TERM
|
||||||
|
|
||||||
|
update_certs "$@"
|
||||||
|
|
||||||
|
# Wait some amount of time
|
||||||
|
echo "Sleep for ${CERTS_UPDATE_INTERVAL}s"
|
||||||
|
sleep $CERTS_UPDATE_INTERVAL & pid=$!
|
||||||
|
wait
|
||||||
|
pid=
|
||||||
59
app/letsencrypt_service_data.tmpl
Normal file
59
app/letsencrypt_service_data.tmpl
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
LETSENCRYPT_CONTAINERS=(
|
||||||
|
{{ range $hosts, $containers := groupBy $ "Env.LETSENCRYPT_HOST" }}
|
||||||
|
{{ if trim $hosts }}
|
||||||
|
{{ range $container := $containers }}
|
||||||
|
{{ if parseBool (coalesce $container.Env.LETSENCRYPT_SINGLE_DOMAIN_CERTS "false") }}
|
||||||
|
{{ range $host := split $hosts "," }}
|
||||||
|
{{ $host := trim $host }}
|
||||||
|
{{- "\t"}}'{{ printf "%.12s" $container.ID }}_{{ sha1 $host }}'
|
||||||
|
{{ end }}
|
||||||
|
{{ else }}
|
||||||
|
{{- "\t"}}'{{ printf "%.12s" $container.ID }}'
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
)
|
||||||
|
|
||||||
|
{{ range $hosts, $containers := groupBy $ "Env.LETSENCRYPT_HOST" }}
|
||||||
|
{{ $hosts := trimSuffix "," $hosts }}
|
||||||
|
{{ range $container := $containers }}
|
||||||
|
{{ $cid := printf "%.12s" $container.ID }}
|
||||||
|
{{ if parseBool (coalesce $container.Env.LETSENCRYPT_SINGLE_DOMAIN_CERTS "false") }}
|
||||||
|
{{ range $host := split $hosts "," }}
|
||||||
|
{{ $host := trim $host }}
|
||||||
|
{{ $host := trimSuffix "." $host }}
|
||||||
|
{{ $hostHash := sha1 $host }}
|
||||||
|
{{- "\n" }}LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_HOST=('{{ $host }}')
|
||||||
|
{{- "\n" }}LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_KEYSIZE="{{ $container.Env.LETSENCRYPT_KEYSIZE }}"
|
||||||
|
{{- "\n" }}LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_TEST="{{ $container.Env.LETSENCRYPT_TEST }}"
|
||||||
|
{{- "\n" }}LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_EMAIL="{{ $container.Env.LETSENCRYPT_EMAIL }}"
|
||||||
|
{{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_CA_URI="{{ $container.Env.ACME_CA_URI }}"
|
||||||
|
{{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_PREFERRED_CHAIN="{{ $container.Env.ACME_PREFERRED_CHAIN }}"
|
||||||
|
{{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_OCSP="{{ $container.Env.ACME_OCSP }}"
|
||||||
|
{{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_EAB_KID="{{ $container.Env.ACME_EAB_KID }}"
|
||||||
|
{{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_EAB_HMAC_KEY="{{ $container.Env.ACME_EAB_HMAC_KEY }}"
|
||||||
|
{{- "\n" }}ZEROSSL_{{ $cid }}_{{ $hostHash }}_API_KEY="{{ $container.Env.ZEROSSL_API_KEY }}"
|
||||||
|
{{- "\n" }}LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_RESTART_CONTAINER="{{ $container.Env.LETSENCRYPT_RESTART_CONTAINER }}"
|
||||||
|
{{ end }}
|
||||||
|
{{ else }}
|
||||||
|
{{- "\n" }}LETSENCRYPT_{{ $cid }}_HOST=(
|
||||||
|
{{- range $host := split $hosts "," }}
|
||||||
|
{{- $host := trim $host }}
|
||||||
|
{{- $host := trimSuffix "." $host -}}
|
||||||
|
'{{ $host }}'{{ " " }}
|
||||||
|
{{- end -}}
|
||||||
|
)
|
||||||
|
{{- "\n" }}LETSENCRYPT_{{ $cid }}_KEYSIZE="{{ $container.Env.LETSENCRYPT_KEYSIZE }}"
|
||||||
|
{{- "\n" }}LETSENCRYPT_{{ $cid }}_TEST="{{ $container.Env.LETSENCRYPT_TEST }}"
|
||||||
|
{{- "\n" }}LETSENCRYPT_{{ $cid }}_EMAIL="{{ $container.Env.LETSENCRYPT_EMAIL }}"
|
||||||
|
{{- "\n" }}ACME_{{ $cid }}_CA_URI="{{ $container.Env.ACME_CA_URI }}"
|
||||||
|
{{- "\n" }}ACME_{{ $cid }}_PREFERRED_CHAIN="{{ $container.Env.ACME_PREFERRED_CHAIN }}"
|
||||||
|
{{- "\n" }}ACME_{{ $cid }}_OCSP="{{ $container.Env.ACME_OCSP }}"
|
||||||
|
{{- "\n" }}ACME_{{ $cid }}_EAB_KID="{{ $container.Env.ACME_EAB_KID }}"
|
||||||
|
{{- "\n" }}ACME_{{ $cid }}_EAB_HMAC_KEY="{{ $container.Env.ACME_EAB_HMAC_KEY }}"
|
||||||
|
{{- "\n" }}ZEROSSL_{{ $cid }}_API_KEY="{{ $container.Env.ZEROSSL_API_KEY }}"
|
||||||
|
{{- "\n" }}LETSENCRYPT_{{ $cid }}_RESTART_CONTAINER="{{ $container.Env.LETSENCRYPT_RESTART_CONTAINER }}"
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
8
app/nginx_location.conf
Normal file
8
app/nginx_location.conf
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
auth_basic off;
|
||||||
|
auth_request off;
|
||||||
|
allow all;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
try_files $uri =404;
|
||||||
|
break;
|
||||||
|
}
|
||||||
4
app/signal_le_service
Executable file
4
app/signal_le_service
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Using busybox pkill
|
||||||
|
pkill -USR1 -f /app/letsencrypt_service
|
||||||
32
app/start.sh
Executable file
32
app/start.sh
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# SIGTERM-handler
|
||||||
|
term_handler() {
|
||||||
|
[[ -n "$docker_gen_pid" ]] && kill "$docker_gen_pid"
|
||||||
|
[[ -n "$letsencrypt_service_pid" ]] && kill "$letsencrypt_service_pid"
|
||||||
|
|
||||||
|
# shellcheck source=functions.sh
|
||||||
|
source /app/functions.sh
|
||||||
|
remove_all_location_configurations
|
||||||
|
remove_all_standalone_configurations
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
trap 'term_handler' INT QUIT TERM
|
||||||
|
|
||||||
|
/app/letsencrypt_service &
|
||||||
|
letsencrypt_service_pid=$!
|
||||||
|
|
||||||
|
wait_default="5s:20s"
|
||||||
|
DOCKER_GEN_WAIT="${DOCKER_GEN_WAIT:-$wait_default}"
|
||||||
|
docker-gen -watch -notify '/app/signal_le_service' -wait "$DOCKER_GEN_WAIT" /app/letsencrypt_service_data.tmpl /app/letsencrypt_service_data &
|
||||||
|
docker_gen_pid=$!
|
||||||
|
|
||||||
|
# wait "indefinitely"
|
||||||
|
while [[ -e /proc/$docker_gen_pid ]]; do
|
||||||
|
wait $docker_gen_pid # Wait for any signals or end of execution of docker-gen
|
||||||
|
done
|
||||||
|
|
||||||
|
# Stop container properly
|
||||||
|
term_handler
|
||||||
48
docker-compose.yml
Normal file
48
docker-compose.yml
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
version: '2'
|
||||||
|
|
||||||
|
services:
|
||||||
|
nginx-proxy:
|
||||||
|
image: nginxproxy/nginx-proxy
|
||||||
|
container_name: nginx-proxy
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
environment:
|
||||||
|
- MAX_UPLOAD_SIZE=500M
|
||||||
|
volumes:
|
||||||
|
- conf:/etc/nginx/conf.d
|
||||||
|
- vhost:/etc/nginx/vhost.d
|
||||||
|
- html:/usr/share/nginx/html
|
||||||
|
- dhparam:/etc/nginx/dhparam
|
||||||
|
- certs:/etc/nginx/certs:ro
|
||||||
|
- ./proxy.conf:/etc/nginx/conf.d/my_proxy.conf:ro
|
||||||
|
- /run/user/1000/docker.sock:/tmp/docker.sock:ro
|
||||||
|
networks:
|
||||||
|
- proxy-tier
|
||||||
|
|
||||||
|
# network_mode: bridge
|
||||||
|
|
||||||
|
acme-companion:
|
||||||
|
image: nginxproxy/acme-companion
|
||||||
|
container_name: nginx-proxy-acme
|
||||||
|
volumes_from:
|
||||||
|
- nginx-proxy
|
||||||
|
volumes:
|
||||||
|
- certs:/etc/nginx/certs:rw
|
||||||
|
- acme:/etc/acme.sh
|
||||||
|
- /run/user/1000/docker.sock:/var/run/docker.sock:ro
|
||||||
|
networks:
|
||||||
|
- proxy-tier
|
||||||
|
|
||||||
|
# network_mode: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
conf:
|
||||||
|
vhost:
|
||||||
|
html:
|
||||||
|
dhparam:
|
||||||
|
certs:
|
||||||
|
acme:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy-tier:
|
||||||
47
docker-compose.yml.franv
Normal file
47
docker-compose.yml.franv
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
version: '2'
|
||||||
|
|
||||||
|
services:
|
||||||
|
nginx-proxy:
|
||||||
|
image: nginxproxy/nginx-proxy
|
||||||
|
container_name: nginx-proxy
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
environment:
|
||||||
|
- MAX_UPLOAD_SIZE=500M
|
||||||
|
volumes:
|
||||||
|
- conf:/etc/nginx/conf.d
|
||||||
|
- vhost:/etc/nginx/vhost.d
|
||||||
|
- html:/usr/share/nginx/html
|
||||||
|
- dhparam:/etc/nginx/dhparam
|
||||||
|
- certs:/etc/nginx/certs:ro
|
||||||
|
- /run/user/1000/docker.sock:/tmp/docker.sock:ro
|
||||||
|
networks:
|
||||||
|
- proxy-tier
|
||||||
|
|
||||||
|
# network_mode: bridge
|
||||||
|
|
||||||
|
acme-companion:
|
||||||
|
image: nginxproxy/acme-companion
|
||||||
|
container_name: nginx-proxy-acme
|
||||||
|
volumes_from:
|
||||||
|
- nginx-proxy
|
||||||
|
volumes:
|
||||||
|
- certs:/etc/nginx/certs:rw
|
||||||
|
- acme:/etc/acme.sh
|
||||||
|
- /run/user/1000/docker.sock:/var/run/docker.sock:ro
|
||||||
|
networks:
|
||||||
|
- proxy-tier
|
||||||
|
|
||||||
|
# network_mode: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
conf:
|
||||||
|
vhost:
|
||||||
|
html:
|
||||||
|
dhparam:
|
||||||
|
certs:
|
||||||
|
acme:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy-tier:
|
||||||
81
docs/Advanced-usage.md
Normal file
81
docs/Advanced-usage.md
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
## Advanced usage (with the nginx and docker-gen containers)
|
||||||
|
|
||||||
|
**nginx-proxy** can also be run as two separate containers using the [nginx-proxy/**docker-gen**](https://github.com/nginx-proxy/docker-gen) image and the official [**nginx**](https://hub.docker.com/_/nginx/) image. You may want to do this to prevent having the docker socket bound to a publicly exposed container service (ie avoid mounting the docker socket in the nginx exposed container).
|
||||||
|
|
||||||
|
**NOTE**: The first time this container is launched in a three container setup, it will generates a new 2048 bits Diffie-Hellman parameters file. This process can take up to several minutes to complete on lower end hosts, and certificates creation won't start before that (be patient).
|
||||||
|
|
||||||
|
Please read and try [basic usage](./Basic-usage.md), and **validate that you have a working two containers setup** before using the three containers setup. In addition to the steps described there, running **nginx-proxy** as two separate containers with **acme-companion** requires the following:
|
||||||
|
|
||||||
|
1) Download and mount the template file [nginx.tmpl](https://github.com/nginx-proxy/nginx-proxy/blob/main/nginx.tmpl) into the **docker-gen** container. You can get the nginx.tmpl file with a command like:
|
||||||
|
|
||||||
|
```
|
||||||
|
curl https://raw.githubusercontent.com/nginx-proxy/nginx-proxy/main/nginx.tmpl > /path/to/nginx.tmpl
|
||||||
|
```
|
||||||
|
|
||||||
|
2) Use the `com.github.jrcs.letsencrypt_nginx_proxy_companion.docker_gen` label on the **docker-gen** container, or explicitly set the `NGINX_DOCKER_GEN_CONTAINER` environment variable on the **acme-companion** container to the name or id of the **docker-gen** container (we'll use the later method in the example).
|
||||||
|
|
||||||
|
3) Declare `/etc/nginx/conf.d` as a volume on the nginx container so that it can be shared with the **docker-gen** container.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
### Step 1 - nginx
|
||||||
|
|
||||||
|
* Start nginx [(official image)](https://hub.docker.com/_/nginx/) with the required volumes:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ docker run --detach \
|
||||||
|
--name nginx-proxy \
|
||||||
|
--publish 80:80 \
|
||||||
|
--publish 443:443 \
|
||||||
|
--volume conf:/etc/nginx/conf.d \
|
||||||
|
--volume vhost:/etc/nginx/vhost.d \
|
||||||
|
--volume html:/usr/share/nginx/html \
|
||||||
|
--volume certs:/etc/nginx/certs \
|
||||||
|
nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2 - docker-gen
|
||||||
|
|
||||||
|
* Start the **docker-gen** container with the shared volumes (with `--volume-from`), the template file and the docker socket:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ docker run --detach \
|
||||||
|
--name nginx-proxy-gen \
|
||||||
|
--volumes-from nginx-proxy \
|
||||||
|
--volume /path/to/nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro \
|
||||||
|
--volume /var/run/docker.sock:/tmp/docker.sock:ro \
|
||||||
|
nginxproxy/docker-gen \
|
||||||
|
-notify-sighup nginx-proxy -watch -wait 5s:30s /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that you must pass the exact name of the **nginx** container to **docker-gen** `-notify-sighup` argument (here `nginx-proxy`).
|
||||||
|
|
||||||
|
|
||||||
|
### Step 3 - acme-companion
|
||||||
|
|
||||||
|
* Start the **acme-companion** container with the `NGINX_DOCKER_GEN_CONTAINER` environment variable correctly set:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ docker run --detach \
|
||||||
|
--name nginx-proxy-acme \
|
||||||
|
--volumes-from nginx-proxy \
|
||||||
|
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||||
|
--volume acme:/etc/acme.sh \
|
||||||
|
--env "NGINX_DOCKER_GEN_CONTAINER=nginx-proxy-gen" \
|
||||||
|
--env "DEFAULT_EMAIL=mail@yourdomain.tld" \
|
||||||
|
nginxproxy/acme-companion
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4 - proxyed container(s)
|
||||||
|
|
||||||
|
* Once the three containers are up, start any containers to be proxied as described in [basic usage](./Basic-usage.md).
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ docker run --detach \
|
||||||
|
--name your-proxyed-app \
|
||||||
|
--env "VIRTUAL_HOST=subdomain.yourdomain.tld" \
|
||||||
|
--env "LETSENCRYPT_HOST=subdomain.yourdomain.tld" \
|
||||||
|
nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are experiencing issues with this setup, fall back to the [basic setup](./Basic-usage.md). The advanced setup is not meant to be obligatory.
|
||||||
83
docs/Basic-usage.md
Normal file
83
docs/Basic-usage.md
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
## Basic usage (with the nginx-proxy container)
|
||||||
|
|
||||||
|
Three writable volumes must be declared on the **nginx-proxy** container so that they can be shared with the **acme-companion** container:
|
||||||
|
|
||||||
|
* `/etc/nginx/certs` to store certificates and private keys (readonly for the **nginx-proxy** container).
|
||||||
|
* `/etc/nginx/vhost.d` to change the configuration of vhosts (required so the CA may access `http-01` challenge files).
|
||||||
|
* `/usr/share/nginx/html` to write `http-01` challenge files.
|
||||||
|
|
||||||
|
Additionally, a fourth volume must be declared on the **acme-companion** container to store `acme.sh` configuration and state: `/etc/acme.sh`.
|
||||||
|
|
||||||
|
Please also read the doc about [data persistence](./Persistent-data.md).
|
||||||
|
|
||||||
|
Example of use:
|
||||||
|
|
||||||
|
### Step 1 - nginx-proxy
|
||||||
|
|
||||||
|
Start **nginx-proxy** with the three additional volumes declared:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ docker run --detach \
|
||||||
|
--name nginx-proxy \
|
||||||
|
--publish 80:80 \
|
||||||
|
--publish 443:443 \
|
||||||
|
--volume certs:/etc/nginx/certs \
|
||||||
|
--volume vhost:/etc/nginx/vhost.d \
|
||||||
|
--volume html:/usr/share/nginx/html \
|
||||||
|
--volume /var/run/docker.sock:/tmp/docker.sock:ro \
|
||||||
|
nginxproxy/nginx-proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
Binding the host docker socket (`/var/run/docker.sock`) inside the container to `/tmp/docker.sock` is a requirement of **nginx-proxy**.
|
||||||
|
|
||||||
|
### Step 2 - acme-companion
|
||||||
|
|
||||||
|
Start the **acme-companion** container, getting the volumes from **nginx-proxy** with `--volumes-from`:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ docker run --detach \
|
||||||
|
--name nginx-proxy-acme \
|
||||||
|
--volumes-from nginx-proxy \
|
||||||
|
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||||
|
--volume acme:/etc/acme.sh \
|
||||||
|
--env "DEFAULT_EMAIL=mail@yourdomain.tld" \
|
||||||
|
nginxproxy/acme-companion
|
||||||
|
```
|
||||||
|
|
||||||
|
The host docker socket has to be bound inside this container too, this time to `/var/run/docker.sock`.
|
||||||
|
|
||||||
|
Albeit **optional**, it is **recommended** to provide a valid default email address through the `DEFAULT_EMAIL` environment variable, so that Let's Encrypt can warn you about expiring certificates and allow you to recover your account.
|
||||||
|
|
||||||
|
### Step 3 - proxyed container(s)
|
||||||
|
|
||||||
|
Once both **nginx-proxy** and **acme-companion** containers are up and running, start any container you want proxyed with environment variables `VIRTUAL_HOST` and `LETSENCRYPT_HOST` both set to the domain(s) your proxyed container is going to use. Multiple hosts can be separated using commas.
|
||||||
|
|
||||||
|
[`VIRTUAL_HOST`](https://github.com/nginx-proxy/nginx-proxy#usage) control proxying by **nginx-proxy** and `LETSENCRYPT_HOST` control certificate creation and SSL enabling by **acme-companion**.
|
||||||
|
|
||||||
|
Certificates will only be issued for containers that have both `VIRTUAL_HOST` and `LETSENCRYPT_HOST` variables set to domain(s) that correctly resolve to the host, provided the host is publicly reachable.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ docker run --detach \
|
||||||
|
--name your-proxyed-app \
|
||||||
|
--env "VIRTUAL_HOST=subdomain.yourdomain.tld" \
|
||||||
|
--env "LETSENCRYPT_HOST=subdomain.yourdomain.tld" \
|
||||||
|
nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
The containers being proxied must expose the port to be proxied, either by using the `EXPOSE` directive in their Dockerfile or by using the `--expose` flag to `docker run` or `docker create`.
|
||||||
|
|
||||||
|
If the proxyed container listen on and expose another port than the default `80`, you can force **nginx-proxy** to use this port with the [`VIRTUAL_PORT`](https://github.com/nginx-proxy/nginx-proxy#multiple-ports) environment variable.
|
||||||
|
|
||||||
|
Example using [Grafana](https://hub.docker.com/r/grafana/grafana/) (expose and listen on port 3000):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ docker run --detach \
|
||||||
|
--name grafana \
|
||||||
|
--env "VIRTUAL_HOST=othersubdomain.yourdomain.tld" \
|
||||||
|
--env "VIRTUAL_PORT=3000" \
|
||||||
|
--env "LETSENCRYPT_HOST=othersubdomain.yourdomain.tld" \
|
||||||
|
--env "LETSENCRYPT_EMAIL=mail@yourdomain.tld" \
|
||||||
|
grafana/grafana
|
||||||
|
```
|
||||||
|
|
||||||
|
Repeat [Step 3](#step-3---proxyed-containers) for any other container you want to proxy.
|
||||||
29
docs/Container-configuration.md
Normal file
29
docs/Container-configuration.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
## Optional container environment variables for custom configuration.
|
||||||
|
|
||||||
|
* `ACME_CA_URI` - Directory URI for the CA ACME API endpoint (defaults to ``https://acme-v02.api.letsencrypt.org/directory``).
|
||||||
|
|
||||||
|
If you set this environment variable value to `https://acme-staging-v02.api.letsencrypt.org/directory` the container will obtain its certificates from Let's Encrypt test API endpoint that don't have the [5 certs/week/domain limit](https://letsencrypt.org/docs/rate-limits/) (but are not trusted by browsers).
|
||||||
|
|
||||||
|
For example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ docker run --detach \
|
||||||
|
--name nginx-proxy-acme \
|
||||||
|
--volumes-from nginx-proxy \
|
||||||
|
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||||
|
--volume certs:/etc/nginx/certs:rw \
|
||||||
|
--volume acme:/etc/acme.sh \
|
||||||
|
--env "ACME_CA_URI=https://acme-staging-v02.api.letsencrypt.org/directory" \
|
||||||
|
nginxproxy/acme-companion
|
||||||
|
```
|
||||||
|
You can also create test certificates per container (see [Test certificates](./Let's-Encrypt-and-ACME.md#test-certificates))
|
||||||
|
|
||||||
|
* `DEBUG` - Set it to `1` to enable debugging of the entrypoint script and generation of LetsEncrypt certificates, which could help you pin point any configuration issues.
|
||||||
|
|
||||||
|
* `RENEW_PRIVATE_KEYS` - Set it to `false` to make `acme.sh` reuse previously generated private key for each certificate instead of creating a new one on certificate renewal. Reusing private keys can help if you intend to use [HPKP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Public_Key_Pinning), but please note that HPKP has been deprecated by Google's Chrome and that it is therefore strongly discouraged to use it at all.
|
||||||
|
|
||||||
|
* `DHPARAM_BITS` - Change the size of the Diffie-Hellman key generated by the container from the default value of 2048 bits. For example `--env DHPARAM_BITS=1024` to support some older clients like Java 6 and 7.
|
||||||
|
|
||||||
|
* `CA_BUNDLE` - This is a test only variable [for use with Pebble](https://github.com/letsencrypt/pebble#avoiding-client-https-errors). It changes the trusted root CA used by `acme.sh`, from the default Alpine trust store to the CA bundle file located at the provided path (inside the container). Do **not** use it in production unless you are running your own ACME CA.
|
||||||
|
|
||||||
|
* `CERTS_UPDATE_INTERVAL` - 3600 seconds by default, this defines how often the container will check if the certificates require update.
|
||||||
23
docs/Container-utilities.md
Normal file
23
docs/Container-utilities.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
The container provide the following utilities (replace `nginx-proxy-acme` with the name or ID of your **acme-companion** container when executing the commands):
|
||||||
|
|
||||||
|
### Force certificates renewal
|
||||||
|
If needed, you can force a running **acme-companion** container to renew all certificates that are currently in use with the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ docker exec nginx-proxy-acme /app/force_renew
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manually trigger the service loop
|
||||||
|
You can trigger the execution of the service loop before the hourly execution with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ docker exec nginx-proxy-acme /app/signal_le_service
|
||||||
|
```
|
||||||
|
Unlike the previous command, this won't force renewal of certificates that don't need to be renewed.
|
||||||
|
|
||||||
|
### Show certificates informations
|
||||||
|
To display informations about your existing certificates, use the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ docker exec nginx-proxy-acme /app/cert_status
|
||||||
|
```
|
||||||
123
docs/Docker-Compose.md
Normal file
123
docs/Docker-Compose.md
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
## Usage with Docker Compose
|
||||||
|
|
||||||
|
As stated by its repository, [Docker Compose](https://github.com/docker/compose) is a tool for defining and running multi-container Docker applications using a single _Compose file_. This Wiki page is not meant to be a definitive reference on how to run **nginx-proxy** and **acme-companion** with Docker Compose, as the number of possible setups is quite extensive and they can't be all covered.
|
||||||
|
|
||||||
|
### Before your start
|
||||||
|
|
||||||
|
Be sure to be familiar with both the [basic](./Basic-usage.md) and [advanced](./Advanced-usage.md) non compose setups, and Docker Compose usage.
|
||||||
|
|
||||||
|
Please read [getting container IDs](./Getting-containers-IDs.md) and be aware that the `--volumes-from method` is **only** available on compose file version 2.
|
||||||
|
|
||||||
|
The following examples are minimal, clean starting points using compose file version 2. Again they are not intended as a definitive reference.
|
||||||
|
|
||||||
|
The use of named containers and volume is not required but helps keeping everything clear and organized.
|
||||||
|
|
||||||
|
### Two containers example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '2'
|
||||||
|
|
||||||
|
services:
|
||||||
|
nginx-proxy:
|
||||||
|
image: nginxproxy/nginx-proxy
|
||||||
|
container_name: nginx-proxy
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- conf:/etc/nginx/conf.d
|
||||||
|
- vhost:/etc/nginx/vhost.d
|
||||||
|
- html:/usr/share/nginx/html
|
||||||
|
- dhparam:/etc/nginx/dhparam
|
||||||
|
- certs:/etc/nginx/certs:ro
|
||||||
|
- /var/run/docker.sock:/tmp/docker.sock:ro
|
||||||
|
network_mode: bridge
|
||||||
|
|
||||||
|
acme-companion:
|
||||||
|
image: nginxproxy/acme-companion
|
||||||
|
container_name: nginx-proxy-acme
|
||||||
|
volumes_from:
|
||||||
|
- nginx-proxy
|
||||||
|
volumes:
|
||||||
|
- certs:/etc/nginx/certs:rw
|
||||||
|
- acme:/etc/acme.sh
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
network_mode: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
conf:
|
||||||
|
vhost:
|
||||||
|
html:
|
||||||
|
dhparam:
|
||||||
|
certs:
|
||||||
|
acme:
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** **nginx-proxy** Dockerfile [create a volume for `/etc/nginx/dhparam`](https://github.com/nginx-proxy/nginx-proxy/blob/e80fc0b304bcbcf703d86392394c1a5adb823e3c/Dockerfile#L34), so this compose file include it as a named volume instead of letting it be created anyway as an anonymous volume.
|
||||||
|
|
||||||
|
### Three containers example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '2'
|
||||||
|
|
||||||
|
services:
|
||||||
|
nginx-proxy:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: nginx-proxy
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- conf:/etc/nginx/conf.d
|
||||||
|
- vhost:/etc/nginx/vhost.d
|
||||||
|
- html:/usr/share/nginx/html
|
||||||
|
- certs:/etc/nginx/certs:ro
|
||||||
|
network_mode: bridge
|
||||||
|
|
||||||
|
docker-gen:
|
||||||
|
image: nginxproxy/docker-gen
|
||||||
|
container_name: nginx-proxy-gen
|
||||||
|
command: -notify-sighup nginx-proxy -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
|
||||||
|
volumes_from:
|
||||||
|
- nginx-proxy
|
||||||
|
volumes:
|
||||||
|
- /path/to/nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro
|
||||||
|
- /var/run/docker.sock:/tmp/docker.sock:ro
|
||||||
|
labels:
|
||||||
|
- "com.github.jrcs.letsencrypt_nginx_proxy_companion.docker_gen"
|
||||||
|
network_mode: bridge
|
||||||
|
|
||||||
|
acme-companion:
|
||||||
|
image: nginxproxy/acme-companion
|
||||||
|
container_name: nginx-proxy-acme
|
||||||
|
volumes_from:
|
||||||
|
- nginx-proxy
|
||||||
|
volumes:
|
||||||
|
- certs:/etc/nginx/certs:rw
|
||||||
|
- acme:/etc/acme.sh
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
network_mode: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
conf:
|
||||||
|
vhost:
|
||||||
|
html:
|
||||||
|
certs:
|
||||||
|
acme:
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** don't forget to replace `/path/to/nginx.tmpl` with the actual path to the [`nginx.tmpl`](https://raw.githubusercontent.com/nginx-proxy/nginx-proxy/main/nginx.tmpl) file you downloaded.
|
||||||
|
|
||||||
|
### Other (external) examples
|
||||||
|
|
||||||
|
**Warning:** some of those examples might be outdated and not working properly with version >= `2.0` of this project.
|
||||||
|
|
||||||
|
If you want other examples how to use this container with Docker Compose, look at:
|
||||||
|
|
||||||
|
* [Nicolas Duchon's Examples](https://github.com/buchdag/letsencrypt-nginx-proxy-companion-compose) - with automated testing
|
||||||
|
* [Evert Ramos's Examples](https://github.com/evertramos/docker-compose-letsencrypt-nginx-proxy-companion) - using docker-compose version '3'
|
||||||
|
* [Karl Fathi's Examples](https://github.com/fatk/docker-letsencrypt-nginx-proxy-companion-examples)
|
||||||
|
* [More examples from Karl](https://github.com/pixelfordinner/pixelcloud-docker-apps/tree/master/nginx-proxy)
|
||||||
|
* [George Ilyes' Examples](https://github.com/gilyes/docker-nginx-letsencrypt-sample)
|
||||||
|
* [Dmitry's simple docker-compose example](https://github.com/dmitrym0/simple-lets-encrypt-docker-compose-sample)
|
||||||
|
* [Radek's docker-compose jenkins example](https://github.com/dataminelab/docker-jenkins-nginx-letsencrypt)
|
||||||
96
docs/Getting-containers-IDs.md
Normal file
96
docs/Getting-containers-IDs.md
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
## Getting nginx-proxy/nginx/docker-gen containers IDs
|
||||||
|
|
||||||
|
For **acme-companion** to work properly, it needs to know the ID of the **nginx**/**nginx-proxy** container (in both [two](./Basic-usage.md) and [three](./Advanced-usage.md) containers setups), plus the ID of the **docker-gen** container in a [three container setup](./Advanced-usage.md).
|
||||||
|
|
||||||
|
There are three methods to inform the **acme-companion** container of the **nginx**/**nginx-proxy** container ID:
|
||||||
|
|
||||||
|
* `label` method: add the label `com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy` to the **nginx**/**nginx-proxy** container.
|
||||||
|
|
||||||
|
* `environment variable` method: assign a fixed name to the **nginx**/**nginx-proxy** container with `container_name:` and set the environment variable `NGINX_PROXY_CONTAINER` to this name on the **acme-companion** container.
|
||||||
|
|
||||||
|
* `volumes_from` method. Using this method, the **acme-companion** container will get the **nginx**/**nginx-proxy** container ID from the volumes it got using the `volumes_from` option.
|
||||||
|
|
||||||
|
And two methods to inform the **acme-companion** container of the **docker-gen** container ID:
|
||||||
|
|
||||||
|
* `label` method: add the label `com.github.jrcs.letsencrypt_nginx_proxy_companion.docker_gen` to the **docker-gen** container.
|
||||||
|
|
||||||
|
* `environment variable` method: assign a fixed name to the **docker-gen** container with `container_name:` and set the environment variable `NGINX_DOCKER_GEN_CONTAINER` to this name on the **acme-companion** container.
|
||||||
|
|
||||||
|
The methods for each container are sorted by order of precedence, meaning that if you use both the label and the volumes_from method, the ID of the **nginx**/**nginx-proxy** container that will be used will be the one found using the label. **There is no point in using more than one method at a time for either the nginx/nginx-proxy or docker-gen container beside potentially confusing yourself**.
|
||||||
|
|
||||||
|
The advantage the `label` methods have over the `environment variable` (and `volumes_from`) methods is enabling the use of the **acme-companion** in environments where containers names are dynamic, like in Swarm Mode or in Docker Cloud. Howhever if you intend to do so, as upstream **docker-gen** lacks the ability to identify containers from labels, you'll need both to either use the two containers setup or to replace nginx-proxy/docker-gen with a fork that has this ability like [herlderco/docker-gen](https://github.com/helderco/docker-gen). Be advised that for now, this works to a very limited extent [(everything has to be on the same node)](https://github.com/nginx-proxy/acme-companion/pull/231#issuecomment-330624331).
|
||||||
|
|
||||||
|
#### Examples with three containers setups:
|
||||||
|
|
||||||
|
`label` method.
|
||||||
|
```
|
||||||
|
$ docker run --detach \
|
||||||
|
[...]
|
||||||
|
--label com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy \
|
||||||
|
nginx
|
||||||
|
|
||||||
|
$ docker run --detach \
|
||||||
|
[...]
|
||||||
|
--label com.github.jrcs.letsencrypt_nginx_proxy_companion.docker_gen \
|
||||||
|
nginxproxy/docker-gen
|
||||||
|
|
||||||
|
$ docker run --detach \
|
||||||
|
[...]
|
||||||
|
nginxproxy/acme-companion
|
||||||
|
```
|
||||||
|
|
||||||
|
`environment variable` method
|
||||||
|
```
|
||||||
|
$ docker run --detach \
|
||||||
|
[...]
|
||||||
|
--name unique-container-name \
|
||||||
|
nginx
|
||||||
|
|
||||||
|
$ docker run --detach \
|
||||||
|
[...]
|
||||||
|
--name another-unique-container-name \
|
||||||
|
nginxproxy/docker-gen
|
||||||
|
|
||||||
|
$ docker run --detach \
|
||||||
|
[...]
|
||||||
|
--env NGINX_PROXY_CONTAINER=unique-container-name \
|
||||||
|
--env NGINX_DOCKER_GEN_CONTAINER=another-unique-container-name \
|
||||||
|
nginxproxy/acme-companion
|
||||||
|
```
|
||||||
|
|
||||||
|
`volumes_from` (**nginx**) + `label` (**docker-gen**) method
|
||||||
|
```
|
||||||
|
$ docker run --detach \
|
||||||
|
[...]
|
||||||
|
--name unique-container-name \
|
||||||
|
nginx
|
||||||
|
|
||||||
|
$ docker run --detach \
|
||||||
|
[...]
|
||||||
|
--label com.github.jrcs.letsencrypt_nginx_proxy_companion.docker_gen \
|
||||||
|
nginxproxy/docker-gen
|
||||||
|
|
||||||
|
$ docker run --detach \
|
||||||
|
[...]
|
||||||
|
--volumes-from unique-container-name \
|
||||||
|
nginxproxy/acme-companion
|
||||||
|
```
|
||||||
|
|
||||||
|
`volumes_from` (**nginx**) + `environment variable` (**docker-gen**) method
|
||||||
|
```
|
||||||
|
$ docker run --detach \
|
||||||
|
[...]
|
||||||
|
--name unique-container-name \
|
||||||
|
nginx
|
||||||
|
|
||||||
|
$ docker run --detach \
|
||||||
|
[...]
|
||||||
|
--name another-unique-container-name \
|
||||||
|
nginxproxy/docker-gen
|
||||||
|
|
||||||
|
$ docker run --detach \
|
||||||
|
[...]
|
||||||
|
--volumes-from unique-container-name \
|
||||||
|
--env NGINX_DOCKER_GEN_CONTAINER=another-unique-container-name \
|
||||||
|
nginxproxy/acme-companion
|
||||||
|
```
|
||||||
74
docs/Invalid-authorizations.md
Normal file
74
docs/Invalid-authorizations.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
## Troubleshooting failing authorizations
|
||||||
|
|
||||||
|
The first two things to do in case of failing authorization are to run the **acme-companion** container with the environment variable `DEBUG=1` to enable the more detailed error messages, and to [request test certificates](./Let's-Encrypt-and-ACME.md#test-certificates) while troubleshooting the issue.
|
||||||
|
|
||||||
|
Common causes of of failing authorizations:
|
||||||
|
|
||||||
|
#### port `80` or `443` on your host are closed / filtered from the outside, possibly because of a misconfigured firewall.
|
||||||
|
|
||||||
|
Check your host `80` and `443` ports **from the outside** (as in from a host having a different public IP) with `nmap` or a similar tool.
|
||||||
|
|
||||||
|
#### your domain name does not resolve to your host IPv4 and/or IPv6.
|
||||||
|
|
||||||
|
Check that your domain name A (and AAAA, if present) records points to the correct adresses using `drill`, `dig` or `nslookup`.
|
||||||
|
|
||||||
|
#### your domain name advertise an AAAA (IPv6) record, but your host or your host's docker isn't actually reachable over IPv6.
|
||||||
|
|
||||||
|
Create a test nginx container on your host and try to reach it over both IPv4 and IPv6.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
you@remotedockerhost$ docker run -d -p 80:80 nginx:alpine
|
||||||
|
|
||||||
|
you@localcomputer$ curl http://your.domain.tld
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Welcome to nginx!</title>
|
||||||
|
[...]
|
||||||
|
</html>
|
||||||
|
|
||||||
|
you@localcomputer$ curl -6 http://your.domain.tld
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Welcome to nginx!</title>
|
||||||
|
[...]
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are unsure of your host/hosts's docker IPv6 connectivity, drop the AAAA record from your domain name and wait for the modification to propagate.
|
||||||
|
|
||||||
|
#### your domain name DNS provider answers incorrectly to CAA record requests.
|
||||||
|
|
||||||
|
Read https://letsencrypt.org/docs/caa/ and test with https://unboundtest.com/
|
||||||
|
|
||||||
|
#### the **nginx-proxy**/**nginx**/**docker-gen**/**acme-companion** containers were misconfigured.
|
||||||
|
|
||||||
|
Review [basic usage](./Basic-usage.md) or [advanced usage](./Advanced-usage.md), plus the [nginx-proxy documentation](https://github.com/nginx-proxy/nginx-proxy).
|
||||||
|
|
||||||
|
Pay special attention to the fact that the volumes **MUST** be shared between the different containers.
|
||||||
|
|
||||||
|
#### you forgot to set both `VIRTUAL_HOST` and `LETSENCRYPT_HOST` on the proxyed container.
|
||||||
|
|
||||||
|
Both are required. Every domain on `LETSENCRYPT_HOST`**must** be on `VIRTUAL_HOST`too.
|
||||||
|
|
||||||
|
#### you are using an outdated version of either **acme-companion** or the nginx.tmpl file (if running a 3 containers setup)
|
||||||
|
|
||||||
|
Pull `nginxproxy/acme-companion:latest` again and get the latest [latest nginx.tmpl](https://raw.githubusercontent.com/nginx-proxy/nginx-proxy/main/nginx.tmpl).
|
||||||
|
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
|
||||||
|
When not in debug mode, the challenge files are automatically cleaned up **after** the authorization process, wether it succeeded or failed, so trying to `curl` them from the outside if you didn't enable debug mode won't yeld any result. If don't want to enable debug mode, you can however create a test file inside the same folder and use it to test the challenge files reachability from the outside (over both IPv4 and IPv6 if you want to use the latter):
|
||||||
|
|
||||||
|
```
|
||||||
|
you@remotedockerhost$ docker exec your-le-container bash -c 'echo "Hello world!" > /usr/share/nginx/html/.well-known/acme-challenge/hello-world'
|
||||||
|
|
||||||
|
you@localcomputer$ curl http://yourdomain.tld/.well-known/acme-challenge/hello-world
|
||||||
|
Hello world!
|
||||||
|
you@localcomputer$ curl -6 http://yourdomain.tld/.well-known/acme-challenge/hello-world
|
||||||
|
Hello world!
|
||||||
|
```
|
||||||
|
|
||||||
|
If you have issues with the [advanced setup](./Advanced-usage.md), fall back to the [basic setup](./Basic-usage.md). The advanced setup is not meant to be obligatory.
|
||||||
97
docs/Let's-Encrypt-and-ACME.md
Normal file
97
docs/Let's-Encrypt-and-ACME.md
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
## Let's Encrypt / ACME
|
||||||
|
|
||||||
|
**NOTE on CAA**: Please ensure that your DNS provider answers correctly to CAA record requests. [If your DNS provider answer with an error, Let's Encrypt won't issue a certificate for your domain](https://letsencrypt.org/docs/caa/). Let's Encrypt do not require that you set a CAA record on your domain, just that your DNS provider answers correctly.
|
||||||
|
|
||||||
|
**NOTE on IPv6**: If the domain or sub domain you want to issue certificate for has an AAAA record set, Let's Encrypt will favor challenge validation over IPv6. [There is an IPv6 to IPv4 fallback in place but Let's Encrypt can't guarantee it'll work in every possible case](https://github.com/letsencrypt/boulder/issues/2770#issuecomment-340489871), so bottom line is **if you are not sure of both your host and your host's Docker reachability over IPv6, do not advertise an AAAA record** or LE challenge validation might fail.
|
||||||
|
|
||||||
|
As described on [basic usage](./Basic-usage.md), the `LETSENCRYPT_HOST` environment variables needs to be declared in each to-be-proxied application containers for which you want to enable SSL and create certificate. It most likely needs to be the same as the `VIRTUAL_HOST` variable and must resolve to your host (which has to be publicly reachable).
|
||||||
|
|
||||||
|
The following environment variables are optional and parametrize the way the Let's Encrypt client works.
|
||||||
|
|
||||||
|
### per proxyed container
|
||||||
|
|
||||||
|
#### Multi-domains certificates
|
||||||
|
|
||||||
|
Specify multiple hosts with a comma delimiter to create multi-domains ([SAN](https://www.digicert.com/subject-alternative-name.htm)) certificates (the first domain in the list will be the base domain).
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ docker run --detach \
|
||||||
|
--name your-proxyed-app \
|
||||||
|
--env "VIRTUAL_HOST=yourdomain.tld,www.yourdomain.tld,anotherdomain.tld" \
|
||||||
|
--env "LETSENCRYPT_HOST=yourdomain.tld,www.yourdomain.tld,anotherdomain.tld" \
|
||||||
|
nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's Encrypt has a limit of [100 domains per certificate](https://letsencrypt.org/fr/docs/rate-limits/), while Buypass limit is [15 domains per certificate](https://www.buypass.com/ssl/products/go-ssl-campaign).
|
||||||
|
|
||||||
|
#### Separate certificate for each domain
|
||||||
|
|
||||||
|
The example above will issue a single domain certificate for all the domains listed in the `LETSENCRYPT_HOST` environment variable. If you need to have a separate certificate for each of the domains, you can add set the `LETSENCRYPT_SINGLE_DOMAIN_CERTS` environment variable to `true`.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ docker run --detach \
|
||||||
|
--name your-proxyed-app \
|
||||||
|
--env "VIRTUAL_HOST=yourdomain.tld,www.yourdomain.tld,anotherdomain.tld" \
|
||||||
|
--env "LETSENCRYPT_HOST=yourdomain.tld,www.yourdomain.tld,anotherdomain.tld" \
|
||||||
|
--env "LETSENCRYPT_SINGLE_DOMAIN_CERTS=true" \
|
||||||
|
nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Automatic certificate renewal
|
||||||
|
Every hour (3600 seconds) the certificates are checked and per default every certificate that have been issued at least [60 days](https://github.com/acmesh-official/acme.sh/blob/f2d350002e7c387fad9777a42cf9befe34996c35/acme.sh#L61) ago is renewed. For Let's Encrypt certificates, that mean they will be renewed 30 days before expiration.
|
||||||
|
|
||||||
|
#### Contact address
|
||||||
|
|
||||||
|
The `LETSENCRYPT_EMAIL` environment variable must be a valid email and will be used by Let's Encrypt to warn you of impeding certificate expiration (should the automated renewal fail) and to recover an account.
|
||||||
|
|
||||||
|
#### Private key size
|
||||||
|
|
||||||
|
The `LETSENCRYPT_KEYSIZE` environment variable determines the type and size of the requested key. Supported values are `2048`, `3072` and `4096` for RSA keys, and `ec-256` or `ec-384` for elliptic curve keys. The default is RSA 4096.
|
||||||
|
|
||||||
|
#### OCSP stapling
|
||||||
|
|
||||||
|
The `ACME_OCSP` environment variable, when set to `true` on a proxied application container, will add the [OCSP Must-Staple extension](https://blog.apnic.net/2019/01/15/is-the-web-ready-for-ocsp-must-staple/) to the issued certificate. Please read about OCSP Must-Staple support in Nginx if you intend to use this feature (https://trac.nginx.org/nginx/ticket/812 and https://trac.nginx.org/nginx/ticket/1830)
|
||||||
|
|
||||||
|
#### Test certificates
|
||||||
|
|
||||||
|
The `LETSENCRYPT_TEST` environment variable, when set to `true` on a proxied application container, will create a test certificates that don't have the [5 certs/week/domain limits](https://letsencrypt.org/docs/rate-limits/) and are signed by an untrusted intermediate (they won't be trusted by browsers).
|
||||||
|
|
||||||
|
If you want to do this globally for all containers, set `ACME_CA_URI` on the **acme-companion** container as described in [Container configuration](./Container-configuration.md).
|
||||||
|
|
||||||
|
#### ACME CA URI
|
||||||
|
|
||||||
|
The `ACME_CA_URI` environment variable is used to set the ACME API endpoint from which the container's certificate(s) will be requested (defaults to ``https://acme-v02.api.letsencrypt.org/directory``).
|
||||||
|
|
||||||
|
#### Preferred chain
|
||||||
|
|
||||||
|
If the ACME CA provides multiple cert chain, you can use the `ACME_PREFERRED_CHAIN` environment variable to select one. See [`acme.sh --preferred-chain` documentation](https://github.com/acmesh-official/acme.sh/wiki/Preferred-Chain) for more info.
|
||||||
|
|
||||||
|
#### Container restart on cert renewal
|
||||||
|
|
||||||
|
The `LETSENCRYPT_RESTART_CONTAINER` environment variable, when set to `true` on an application container, will restart this container whenever the corresponding cert (`LETSENCRYPT_HOST`) is renewed. This is useful when certificates are directly used inside a container for other purposes than HTTPS (e.g. an FTPS server), to make sure those containers always use an up to date certificate.
|
||||||
|
|
||||||
|
### global (set on acme-companion container)
|
||||||
|
|
||||||
|
#### Default contact address
|
||||||
|
|
||||||
|
The `DEFAULT_EMAIL` variable must be a valid email and, when set on the **acme-companion** container, will be used as a fallback when no email address is provided using proxyed container's `LETSENCRYPT_EMAIL` environment variables. It is highly recommended to set this variable to a valid email address that you own.
|
||||||
|
|
||||||
|
#### Private key re-utilization
|
||||||
|
|
||||||
|
The `RENEW_PRIVATE_KEYS` environment variable, when set to `false` on the **acme-companion** container, will set `acme.sh` to reuse previously generated private key instead of generating a new one at renewal for all domains.
|
||||||
|
|
||||||
|
Reusing private keys can help if you intend to use [HPKP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Public_Key_Pinning), but please note that HPKP has been deprecated by Google's Chrome and that it is therefore strongly discouraged to use it at all.
|
||||||
|
|
||||||
|
#### ACME accounts handling
|
||||||
|
|
||||||
|
- Use one `acme.sh` configuration directory (`--config-home`) per account email address.
|
||||||
|
- Each `acme.sh` configuration directory can hold several accounts on different ACME service providers. But only one per service provider.
|
||||||
|
- The `default` configuration directory holds the configuration for empty account email address.
|
||||||
|
- When in testing mode (`LETSENCRYPT_TEST=true`):
|
||||||
|
1. The container will use the special purpose `staging` configuration directory.
|
||||||
|
1. The directory URI is forced to The Let's Encrypt v2 staging one (`ACME_CA_URI` is ignored)
|
||||||
|
2. The account email address is forced empty (`DEFAULT_EMAIL` and `LETSENCRYPT_EMAIL` are ignored)
|
||||||
120
docs/Persistent-data.md
Normal file
120
docs/Persistent-data.md
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
## Persistent data
|
||||||
|
|
||||||
|
### Named volumes (recommended)
|
||||||
|
|
||||||
|
When you follow instructions from Basic usage or Advanced usage, Docker will automatically create **named volumes** for every `--volume` / `-v` argument passed. Named volume will make it easy for you to mount the same persisted data even if you delete then re-create the container:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ docker run -d \
|
||||||
|
--name nginx-proxy \
|
||||||
|
-p 80:80 \
|
||||||
|
-p 443:443 \
|
||||||
|
-v certs:/etc/nginx/certs \
|
||||||
|
-v vhost:/etc/nginx/vhost.d \
|
||||||
|
-v html:/usr/share/nginx/html \
|
||||||
|
-v /var/run/docker.sock:/tmp/docker.sock:ro \
|
||||||
|
nginxproxy/nginx-proxy
|
||||||
|
|
||||||
|
$ docker volume ls
|
||||||
|
DRIVER VOLUME NAME
|
||||||
|
local certs
|
||||||
|
local vhost
|
||||||
|
local html
|
||||||
|
```
|
||||||
|
|
||||||
|
### Anonymous volumes (not recommended)
|
||||||
|
|
||||||
|
If you don't prefix your volumes with a name, Docker will instead create **anonymous volumes** (volumes with a random name). Those volume persist after the container is deleted but aren't automatically re-mounted when you re-create the container. Their usage is **not recommended** as they don't provide any advantages over named volumes and make keeping tracks of what volume store which data way harder.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ docker run -d \
|
||||||
|
--name nginx-proxy \
|
||||||
|
-p 80:80 \
|
||||||
|
-p 443:443 \
|
||||||
|
-v /etc/nginx/certs \
|
||||||
|
-v /etc/nginx/vhost.d \
|
||||||
|
-v /usr/share/nginx/html \
|
||||||
|
-v /var/run/docker.sock:/tmp/docker.sock:ro \
|
||||||
|
nginxproxy/nginx-proxy
|
||||||
|
|
||||||
|
$ docker volume ls
|
||||||
|
DRIVER VOLUME NAME
|
||||||
|
local 287be3abd610e5566500d719ceb8b952952f12c9324ef02d05785d4ee9737ae9
|
||||||
|
local 6530b1b40cf89efb71aa7fd19bddec927fa2bcae59b04b9c1c850af72ffe0123
|
||||||
|
local f260f71fefadcdfc311d285d69151f2312915174d3fb1fab89949ec5ec871a54
|
||||||
|
```
|
||||||
|
|
||||||
|
### Host volumes
|
||||||
|
|
||||||
|
Alternatively, you might want to store the certificates on a local folder rather than letting Docker create and manage a volume for them. This is easily achieved by using a **host volume** (binding an absolute path on your host to the `/ect/nginx/certs` folder on your containers):
|
||||||
|
|
||||||
|
`-v /path/to/certificates:/etc/nginx/certs`
|
||||||
|
|
||||||
|
No matter the type of volume you choose, if you set them on the nginx-proxy or nginx container and use `--volumes_from` on the others containers, they will automatically be mounted inside the container to the path your first defined.
|
||||||
|
|
||||||
|
### Restraining other containers write permission
|
||||||
|
|
||||||
|
If you want to restrain the **nginx** and **docker-gen** processes to read only access on the certificates, you'll have to use different volume flags depending on the container.
|
||||||
|
|
||||||
|
Example with named volumes:
|
||||||
|
|
||||||
|
`-v certs:/etc/nginx/certs:ro` on the **nginx-proxy** or **nginx** + **docker-gen** container(s).
|
||||||
|
|
||||||
|
`-v certs:/etc/nginx/certs:rw` on the **acme-companion** container.
|
||||||
|
|
||||||
|
## Ownership & permissions of private and ACME account keys
|
||||||
|
|
||||||
|
By default, the **acme-companion** container will enforce the following ownership and permissions scheme on the files it creates and manage:
|
||||||
|
|
||||||
|
```
|
||||||
|
[drwxr-xr-x] /etc/nginx/certs
|
||||||
|
├── [drwxr-xr-x root root] accounts
|
||||||
|
│ └── [drwxr-xr-x root root] acme-v02.api.letsencrypt.org
|
||||||
|
│ └── [drwxr-xr-x root root] directory
|
||||||
|
│ └── [-rw-r--r-- root root] default.json
|
||||||
|
├── [-rw-r--r-- root root] dhparam.pem
|
||||||
|
├── [-rw-r--r-- root root] default.crt
|
||||||
|
├── [-rw-r--r-- root root] default.key
|
||||||
|
├── [drwxr-xr-x root root] domain.tld
|
||||||
|
│ ├── [lrwxrwxrwx root root] account_key.json -> ../accounts/acme-v02.api.letsencrypt.org/directory/default.json
|
||||||
|
│ ├── [-rw-r--r-- root root] cert.pem
|
||||||
|
│ ├── [-rw-r--r-- root root] chain.pem
|
||||||
|
│ ├── [-rw-r--r-- root root] fullchain.pem
|
||||||
|
│ └── [-rw-r--r-- root root] key.pem
|
||||||
|
├── [lrwxrwxrwx root root] domain.tld.chain.pem -> ./domain.tld/chain.pem
|
||||||
|
├── [lrwxrwxrwx root root] domain.tld.crt -> ./domain.tld/fullchain.pem
|
||||||
|
├── [lrwxrwxrwx root root] domain.tld.dhparam.pem -> ./dhparam.pem
|
||||||
|
└── [lrwxrwxrwx root root] domain.tld.key -> ./domain.tld/key.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
This behavior can be customized using the following environment variable on the **acme-companion** container:
|
||||||
|
|
||||||
|
* `FILES_UID` - Set the user owning the files and folders managed by the container. The variable can be either a user name if this user exists inside the container or a user numeric ID. Default to `root` (user ID `0`).
|
||||||
|
* `FILES_GID` - Set the group owning the files and folders managed by the container. The variable can be either a group name if this group exists inside the container or a group numeric ID. Default to the same value as `FILES_UID`.
|
||||||
|
* `FILES_PERMS` - Set the permissions of the private keys and ACME account keys. The variable must be a valid octal permission setting and defaults to `644`.
|
||||||
|
* `FOLDERS_PERMS` - Set the permissions of the folders managed by the container. The variable must be a valid octal permission setting and defaults to `755`.
|
||||||
|
|
||||||
|
For example, `FILES_UID=1000`, `FILES_PERMS=600` and `FOLDERS_PERMS=700` will result in the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
[drwxr-xr-x] /etc/nginx/certs
|
||||||
|
├── [drwx------ 1000 1000] accounts
|
||||||
|
│ └── [drwx------ 1000 1000] acme-v02.api.letsencrypt.org
|
||||||
|
│ └── [drwx------ 1000 1000] directory
|
||||||
|
│ └── [-rw------- 1000 1000] default.json
|
||||||
|
├── [-rw-r--r-- 1000 1000] dhparam.pem
|
||||||
|
├── [-rw-r--r-- 1000 1000] default.crt
|
||||||
|
├── [-rw------- 1000 1000] default.key
|
||||||
|
├── [drwx------ 1000 1000] domain.tld
|
||||||
|
│ ├── [lrwxrwxrwx 1000 1000] account_key.json -> ../accounts/acme-v02.api.letsencrypt.org/directory/default.json
|
||||||
|
│ ├── [-rw-r--r-- 1000 1000] cert.pem
|
||||||
|
│ ├── [-rw-r--r-- 1000 1000] chain.pem
|
||||||
|
│ ├── [-rw-r--r-- 1000 1000] fullchain.pem
|
||||||
|
│ └── [-rw------- 1000 1000] key.pem
|
||||||
|
├── [lrwxrwxrwx 1000 1000] domain.tld.chain.pem -> ./domain.tld/chain.pem
|
||||||
|
├── [lrwxrwxrwx 1000 1000] domain.tld.crt -> ./domain.tld/fullchain.pem
|
||||||
|
├── [lrwxrwxrwx 1000 1000] domain.tld.dhparam.pem -> ./dhparam.pem
|
||||||
|
└── [lrwxrwxrwx 1000 1000] domain.tld.key -> ./domain.tld/key.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
If you just want to make the most sensitive files (private keys and ACME account keys) root readable only, set the environment variable `FILES_PERMS` to `600` on your **acme-companion** container.
|
||||||
27
docs/README.md
Normal file
27
docs/README.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
#### Usage:
|
||||||
|
|
||||||
|
[Basic (two containers).](./Basic-usage.md)
|
||||||
|
|
||||||
|
[Advanced (three containers).](./Advanced-usage.md)
|
||||||
|
|
||||||
|
[Getting containers IDs](./Getting-containers-IDs.md)
|
||||||
|
|
||||||
|
[with Docker Compose](./Docker-Compose.md)
|
||||||
|
|
||||||
|
[Container utilities](./Container-utilities.md)
|
||||||
|
|
||||||
|
#### Additional configuration:
|
||||||
|
|
||||||
|
[Let's Encrypt / ACME](./Let's-Encrypt-and-ACME.md)
|
||||||
|
|
||||||
|
[Container configuration](./Container-configuration.md)
|
||||||
|
|
||||||
|
[Persistent data](./Persistent-data.md)
|
||||||
|
|
||||||
|
[Standalone certificates](./Standalone-certificates.md)
|
||||||
|
|
||||||
|
[Zero SSL](./Zero-SSL.md)
|
||||||
|
|
||||||
|
#### Troubleshooting:
|
||||||
|
|
||||||
|
[Invalid / failing authorizations](./Invalid-authorizations.md)
|
||||||
73
docs/Standalone-certificates.md
Normal file
73
docs/Standalone-certificates.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
## Standalone certificates
|
||||||
|
|
||||||
|
You can generate certificate that are not tied to containers environment variable by mounting a user configuration file inside the container at `/app/letsencrypt_user_data`. This feature also require sharing the `/etc/nginx/conf.d` folder between the **nginx-proxy** and **acme-companion** container (and the **docker-gen** container if you are running a [three container setup](./Advanced-usage.md)):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ docker run --detach \
|
||||||
|
--name nginx-proxy \
|
||||||
|
--publish 80:80 \
|
||||||
|
--publish 443:443 \
|
||||||
|
--volume certs:/etc/nginx/certs \
|
||||||
|
--volume vhost:/etc/nginx/vhost.d \
|
||||||
|
--volume conf:/etc/nginx/conf.d \
|
||||||
|
--volume html:/usr/share/nginx/html \
|
||||||
|
--volume /var/run/docker.sock:/tmp/docker.sock:ro \
|
||||||
|
nginxproxy/nginx-proxy
|
||||||
|
```
|
||||||
|
```bash
|
||||||
|
$ docker run --detach \
|
||||||
|
--name nginx-proxy-acme \
|
||||||
|
--volumes-from nginx-proxy \
|
||||||
|
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||||
|
--volume acme:/etc/acme.sh \
|
||||||
|
--volume /path/to/your/config_file:/app/letsencrypt_user_data:ro \
|
||||||
|
nginxproxy/acme-companion
|
||||||
|
```
|
||||||
|
|
||||||
|
The user configuration file is a collection of bash variables and array, and follows the syntax of the `/app/letsencrypt_service_data` file that get created by **docker-gen**.
|
||||||
|
|
||||||
|
### Required configuration parameters:
|
||||||
|
|
||||||
|
`LETSENCRYPT_STANDALONE_CERTS` : a bash array containing identifier(s) for you standalone certificate(s). Each element in the array has to be unique. Those identifiers are internal to the container process and won't ever be visible to the outside world or appear on your certificate.
|
||||||
|
|
||||||
|
`LETSENCRYPT_uniqueidentifier_HOST` : a bash array containing domain(s) that will be covered by the certificate corresponding to `uniqueidentifier`.
|
||||||
|
|
||||||
|
Each identifier in `LETSENCRYPT_STANDALONE_CERTS` must have its own corresponding `LETSENCRYPT_uniqueidentifier_HOST` array.
|
||||||
|
|
||||||
|
**Minimal example generating a single certificate for a single domain:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LETSENCRYPT_STANDALONE_CERTS=('uniqueidentifier')
|
||||||
|
LETSENCRYPT_uniqueidentifier_HOST=('yourdomain.tld')
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example with multiple certificates and domains:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LETSENCRYPT_STANDALONE_CERTS=('web' 'app' 'othersite')
|
||||||
|
LETSENCRYPT_web_HOST=('yourdomain.tld' 'www.yourdomain.tld')
|
||||||
|
LETSENCRYPT_app_HOST=('myapp.yourdomain.tld' 'myapp.yourotherdomain.tld' 'service.yourotherdomain.tld')
|
||||||
|
LETSENCRYPT_othersite_HOST=('yetanotherdomain.tld')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optional configuration parameters:
|
||||||
|
|
||||||
|
Those are all single bash variables.
|
||||||
|
|
||||||
|
`LETSENCRYPT_uniqueidentifier_EMAIL` : must be a valid email and will be used by Let's Encrypt to warn you of impeding certificate expiration (should the automated renewal fail).
|
||||||
|
|
||||||
|
`LETSENCRYPT_uniqueidentifier_KEYSIZE` : determines the size of the requested private key. See [private key size](./Let's-Encrypt-and-ACME.md#private-key-size) for accepted values.
|
||||||
|
|
||||||
|
`LETSENCRYPT_uniqueidentifier_TEST` : if set to true, the corresponding certificate will be a test certificates: it won't have the 5 certs/week/domain limits and will be signed by an untrusted intermediate (ie it won't be trusted by browsers).
|
||||||
|
|
||||||
|
### Picking up changes to letsencrypt_user_data
|
||||||
|
|
||||||
|
The container does not actively watch the `/app/letsencrypt_user_data` file for changes.
|
||||||
|
|
||||||
|
Changes will either be picked up every hour when the service loop execute again, or by using `docker exec your-le-container-name-or-id /app/signal_le_service` to manually trigger the service loop execution.
|
||||||
|
|
||||||
|
### Proxying to something else than a Docker container
|
||||||
|
|
||||||
|
Please see the [**nginx-proxy** documentation](https://github.com/nginx-proxy/nginx-proxy#proxy-wide).
|
||||||
|
|
||||||
|
No support will be provided on the **acme-companion** repo for proxying related issues or questions.
|
||||||
22
docs/Zero-SSL.md
Normal file
22
docs/Zero-SSL.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
## Zero SSL
|
||||||
|
|
||||||
|
[Zero SSL](https://zerossl.com/) is an ACME CA that offer some advantages over Let's Encrypt:
|
||||||
|
- no staging endpoint and [no rate limiting on the production endpoint](https://zerossl.com/features/acme/).
|
||||||
|
- web based [management console](https://zerossl.com/features/console/) to keep track of your SSL certificates.
|
||||||
|
|
||||||
|
Using Zero SSL through an ACME client, like in this container, allows for unlimited 90 days and multi-domains (SAN) certificates.
|
||||||
|
|
||||||
|
### Activation
|
||||||
|
|
||||||
|
The Zero SSL support is activated when the `ACME_CA_URI` environment variable is set to the Zero SSL ACME endpoint (`https://acme.zerossl.com/v2/DV90`).
|
||||||
|
|
||||||
|
### Account
|
||||||
|
|
||||||
|
Unlike Let's Encrypt, Zero SSL requires the use of an email bound account. If you already created a Zero SSL account, you can either:
|
||||||
|
|
||||||
|
- provide pre-generated [EAB credentials](https://tools.ietf.org/html/rfc8555#section-7.3.4) using the `ACME_EAB_KID` and `ACME_EAB_HMAC_KEY` environment variables.
|
||||||
|
- provide your ZeroSSL API key using the `ZEROSSL_API_KEY` environment variable.
|
||||||
|
|
||||||
|
These variables can be set on the proxied containers or directly on the **acme-companion** container.
|
||||||
|
|
||||||
|
If you don't have a ZeroSSL account, you can let **acme-companion** create a Zero SSL account with the adress provided in the `ACME_EMAIL` or `DEFAULT_EMAIL` environment variable. Note that the adresse that will be used must be a valid email adress that you actually own.
|
||||||
26
install_acme.sh
Executable file
26
install_acme.sh
Executable file
@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Install git (required to fetch acme.sh)
|
||||||
|
apk --no-cache --virtual .acmesh-deps add git
|
||||||
|
|
||||||
|
# Get acme.sh ACME client source
|
||||||
|
mkdir /src
|
||||||
|
git -C /src clone https://github.com/Neilpang/acme.sh.git
|
||||||
|
cd /src/acme.sh
|
||||||
|
if [[ "$ACMESH_VERSION" != "master" ]]; then
|
||||||
|
git -c advice.detachedHead=false checkout "$ACMESH_VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install acme.sh in /app
|
||||||
|
./acme.sh --install \
|
||||||
|
--nocron \
|
||||||
|
--auto-upgrade 0 \
|
||||||
|
--home /app \
|
||||||
|
--config-home /etc/acme.sh/default
|
||||||
|
|
||||||
|
# Make house cleaning
|
||||||
|
cd /
|
||||||
|
rm -rf /src
|
||||||
|
apk del .acmesh-deps
|
||||||
1
proxy.conf
Normal file
1
proxy.conf
Normal file
@ -0,0 +1 @@
|
|||||||
|
client_max_body_size 500m;
|
||||||
BIN
schema.png
Normal file
BIN
schema.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
36
test/README.md
Normal file
36
test/README.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
### acme-companion test suite
|
||||||
|
|
||||||
|
The test suite can be run locally on a Linux or macOS host.
|
||||||
|
|
||||||
|
To prepare the test setup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/nginx-proxy/acme-companion.git
|
||||||
|
cd acme-companion
|
||||||
|
test/setup/setup-local.sh --setup
|
||||||
|
```
|
||||||
|
|
||||||
|
Then build the docker image and run the tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t nginxproxy/acme-companion .
|
||||||
|
test/run.sh nginxproxy/acme-companion
|
||||||
|
```
|
||||||
|
|
||||||
|
You can limit the test run to specific test(s) with the `-t` flag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
test/run.sh -t docker_api nginxproxy/acme-companion
|
||||||
|
```
|
||||||
|
|
||||||
|
When running the test suite, the standard output of each individual test is captured and compared to its expected-std-out.txt file. When developing or modifying a test, you can use the `-d` flag to disable the standard output capture by the test suite.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
test/run.sh -d nginxproxy/acme-companion
|
||||||
|
```
|
||||||
|
|
||||||
|
To remove the test setup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
test/setup/setup-local.sh --teardown
|
||||||
|
```
|
||||||
26
test/config.sh
Executable file
26
test/config.sh
Executable file
@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
globalTests+=(
|
||||||
|
docker_api
|
||||||
|
location_config
|
||||||
|
default_cert
|
||||||
|
certs_single
|
||||||
|
certs_san
|
||||||
|
certs_single_domain
|
||||||
|
certs_standalone
|
||||||
|
force_renew
|
||||||
|
acme_accounts
|
||||||
|
private_keys
|
||||||
|
container_restart
|
||||||
|
permissions_default
|
||||||
|
permissions_custom
|
||||||
|
symlinks
|
||||||
|
)
|
||||||
|
|
||||||
|
# The ocsp_must_staple test does not work with Pebble
|
||||||
|
if [[ "$ACME_CA" == 'boulder' ]]; then
|
||||||
|
globalTests+=(
|
||||||
|
ocsp_must_staple
|
||||||
|
)
|
||||||
|
fi
|
||||||
25
test/github_actions/containers-logs.sh
Executable file
25
test/github_actions/containers-logs.sh
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
bold_echo() {
|
||||||
|
echo -e "\033[33;1m$1\033[0m"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -f "$GITHUB_WORKSPACE/test/github_actions/failed_tests.txt" ]]; then
|
||||||
|
mapfile -t containers < "$GITHUB_WORKSPACE/test/github_actions/failed_tests.txt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
containers+=("$NGINX_CONTAINER_NAME")
|
||||||
|
[[ $SETUP = "3containers" ]] && containers+=("$DOCKER_GEN_CONTAINER_NAME")
|
||||||
|
[[ $ACME_CA = "boulder" ]] && containers+=(boulder)
|
||||||
|
[[ $ACME_CA = "pebble" ]] && containers+=(pebble challtestserv)
|
||||||
|
|
||||||
|
for container in "${containers[@]}"; do
|
||||||
|
bold_echo "Docker container output for $container"
|
||||||
|
docker logs "$container"
|
||||||
|
docker inspect "$container"
|
||||||
|
if [[ "$container" == "acme_accounts" ]]; then
|
||||||
|
bold_echo "Docker container output for ${container}_default"
|
||||||
|
docker logs "${container}_default"
|
||||||
|
docker inspect "${container}_default"
|
||||||
|
fi
|
||||||
|
done
|
||||||
458
test/run.sh
Executable file
458
test/run.sh
Executable file
@ -0,0 +1,458 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#shellcheck disable=SC2068,SC2206
|
||||||
|
|
||||||
|
# This file is adapted from the Docker official images test suite
|
||||||
|
# under Apache 2.0 License and as such includes of copy of the license
|
||||||
|
# https://github.com/docker-library/official-images/tree/master/test
|
||||||
|
#
|
||||||
|
# Apache License
|
||||||
|
# Version 2.0, January 2004
|
||||||
|
# http://www.apache.org/licenses/
|
||||||
|
#
|
||||||
|
# TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
#
|
||||||
|
# 1. Definitions.
|
||||||
|
#
|
||||||
|
# "License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
# and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
#
|
||||||
|
# "Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
# the copyright owner that is granting the License.
|
||||||
|
#
|
||||||
|
# "Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
# other entities that control, are controlled by, or are under common
|
||||||
|
# control with that entity. For the purposes of this definition,
|
||||||
|
# "control" means (i) the power, direct or indirect, to cause the
|
||||||
|
# direction or management of such entity, whether by contract or
|
||||||
|
# otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
# outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
#
|
||||||
|
# "You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
# exercising permissions granted by this License.
|
||||||
|
#
|
||||||
|
# "Source" form shall mean the preferred form for making modifications,
|
||||||
|
# including but not limited to software source code, documentation
|
||||||
|
# source, and configuration files.
|
||||||
|
#
|
||||||
|
# "Object" form shall mean any form resulting from mechanical
|
||||||
|
# transformation or translation of a Source form, including but
|
||||||
|
# not limited to compiled object code, generated documentation,
|
||||||
|
# and conversions to other media types.
|
||||||
|
#
|
||||||
|
# "Work" shall mean the work of authorship, whether in Source or
|
||||||
|
# Object form, made available under the License, as indicated by a
|
||||||
|
# copyright notice that is included in or attached to the work
|
||||||
|
# (an example is provided in the Appendix below).
|
||||||
|
#
|
||||||
|
# "Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
# form, that is based on (or derived from) the Work and for which the
|
||||||
|
# editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
# represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
# of this License, Derivative Works shall not include works that remain
|
||||||
|
# separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
# the Work and Derivative Works thereof.
|
||||||
|
#
|
||||||
|
# "Contribution" shall mean any work of authorship, including
|
||||||
|
# the original version of the Work and any modifications or additions
|
||||||
|
# to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
# submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
# or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
# the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
# means any form of electronic, verbal, or written communication sent
|
||||||
|
# to the Licensor or its representatives, including but not limited to
|
||||||
|
# communication on electronic mailing lists, source code control systems,
|
||||||
|
# and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
# Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
# excluding communication that is conspicuously marked or otherwise
|
||||||
|
# designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
#
|
||||||
|
# "Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
# on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
# subsequently incorporated within the Work.
|
||||||
|
#
|
||||||
|
# 2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
# this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
# worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
# copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
# publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
# Work and such Derivative Works in Source or Object form.
|
||||||
|
#
|
||||||
|
# 3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
# this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
# worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
# (except as stated in this section) patent license to make, have made,
|
||||||
|
# use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
# where such license applies only to those patent claims licensable
|
||||||
|
# by such Contributor that are necessarily infringed by their
|
||||||
|
# Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
# with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
# institute patent litigation against any entity (including a
|
||||||
|
# cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
# or a Contribution incorporated within the Work constitutes direct
|
||||||
|
# or contributory patent infringement, then any patent licenses
|
||||||
|
# granted to You under this License for that Work shall terminate
|
||||||
|
# as of the date such litigation is filed.
|
||||||
|
#
|
||||||
|
# 4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
# Work or Derivative Works thereof in any medium, with or without
|
||||||
|
# modifications, and in Source or Object form, provided that You
|
||||||
|
# meet the following conditions:
|
||||||
|
#
|
||||||
|
# (a) You must give any other recipients of the Work or
|
||||||
|
# Derivative Works a copy of this License; and
|
||||||
|
#
|
||||||
|
# (b) You must cause any modified files to carry prominent notices
|
||||||
|
# stating that You changed the files; and
|
||||||
|
#
|
||||||
|
# (c) You must retain, in the Source form of any Derivative Works
|
||||||
|
# that You distribute, all copyright, patent, trademark, and
|
||||||
|
# attribution notices from the Source form of the Work,
|
||||||
|
# excluding those notices that do not pertain to any part of
|
||||||
|
# the Derivative Works; and
|
||||||
|
#
|
||||||
|
# (d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
# distribution, then any Derivative Works that You distribute must
|
||||||
|
# include a readable copy of the attribution notices contained
|
||||||
|
# within such NOTICE file, excluding those notices that do not
|
||||||
|
# pertain to any part of the Derivative Works, in at least one
|
||||||
|
# of the following places: within a NOTICE text file distributed
|
||||||
|
# as part of the Derivative Works; within the Source form or
|
||||||
|
# documentation, if provided along with the Derivative Works; or,
|
||||||
|
# within a display generated by the Derivative Works, if and
|
||||||
|
# wherever such third-party notices normally appear. The contents
|
||||||
|
# of the NOTICE file are for informational purposes only and
|
||||||
|
# do not modify the License. You may add Your own attribution
|
||||||
|
# notices within Derivative Works that You distribute, alongside
|
||||||
|
# or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
# that such additional attribution notices cannot be construed
|
||||||
|
# as modifying the License.
|
||||||
|
#
|
||||||
|
# You may add Your own copyright statement to Your modifications and
|
||||||
|
# may provide additional or different license terms and conditions
|
||||||
|
# for use, reproduction, or distribution of Your modifications, or
|
||||||
|
# for any such Derivative Works as a whole, provided Your use,
|
||||||
|
# reproduction, and distribution of the Work otherwise complies with
|
||||||
|
# the conditions stated in this License.
|
||||||
|
#
|
||||||
|
# 5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
# any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
# by You to the Licensor shall be under the terms and conditions of
|
||||||
|
# this License, without any additional terms or conditions.
|
||||||
|
# Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
# the terms of any separate license agreement you may have executed
|
||||||
|
# with Licensor regarding such Contributions.
|
||||||
|
#
|
||||||
|
# 6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
# names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
# except as required for reasonable and customary use in describing the
|
||||||
|
# origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
#
|
||||||
|
# 7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
# agreed to in writing, Licensor provides the Work (and each
|
||||||
|
# Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied, including, without limitation, any warranties or conditions
|
||||||
|
# of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
# PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
# appropriateness of using or redistributing the Work and assume any
|
||||||
|
# risks associated with Your exercise of permissions under this License.
|
||||||
|
#
|
||||||
|
# 8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
# whether in tort (including negligence), contract, or otherwise,
|
||||||
|
# unless required by applicable law (such as deliberate and grossly
|
||||||
|
# negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
# liable to You for damages, including any direct, indirect, special,
|
||||||
|
# incidental, or consequential damages of any character arising as a
|
||||||
|
# result of this License or out of the use or inability to use the
|
||||||
|
# Work (including but not limited to damages for loss of goodwill,
|
||||||
|
# work stoppage, computer failure or malfunction, or any and all
|
||||||
|
# other commercial damages or losses), even if such Contributor
|
||||||
|
# has been advised of the possibility of such damages.
|
||||||
|
#
|
||||||
|
# 9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
# the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
# and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
# or other liability obligations and/or rights consistent with this
|
||||||
|
# License. However, in accepting such obligations, You may act only
|
||||||
|
# on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
# of any other Contributor, and only if You agree to indemnify,
|
||||||
|
# defend, and hold each Contributor harmless for any liability
|
||||||
|
# incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
# of your accepting any such warranty or additional liability.
|
||||||
|
#
|
||||||
|
# END OF TERMS AND CONDITIONS
|
||||||
|
#
|
||||||
|
# Copyright 2014 Docker, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
## Next twelve lines were added by nginxproxy/acme-companion
|
||||||
|
dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
self="$(basename "$0")"
|
||||||
|
failed_tests=()
|
||||||
|
|
||||||
|
if [[ -z $GITHUB_ACTIONS ]] && [[ -f "$dir/local_test_env.sh" ]]; then
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$dir/local_test_env.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck source=./tests/test-functions.sh
|
||||||
|
source "$dir/tests/test-functions.sh"
|
||||||
|
## End of additional code
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOUSAGE
|
||||||
|
|
||||||
|
usage: $self [-t test ...] image:tag [...]
|
||||||
|
ie: $self debian:stretch
|
||||||
|
$self -t utc python:3
|
||||||
|
$self -t utc python:3 -t python-hy
|
||||||
|
|
||||||
|
This script processes the specified Docker images to test their running
|
||||||
|
environments.
|
||||||
|
EOUSAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
# arg handling
|
||||||
|
## Next nine lines were added or modified by nginxproxy/acme-companion
|
||||||
|
case "$(uname)" in
|
||||||
|
Linux)
|
||||||
|
opts="$(getopt -o 'hdkt:c:?' --long 'dry-run,help,test:,config:,keep-namespace' -- "$@" || { usage >&2 && false; })"
|
||||||
|
;;
|
||||||
|
|
||||||
|
Darwin)
|
||||||
|
opts="$(getopt hdkt:c:? "$@" || { usage >&2 && false; })"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
## End of additional or modified code
|
||||||
|
eval set -- "$opts"
|
||||||
|
|
||||||
|
declare -A argTests=()
|
||||||
|
declare -a configs=()
|
||||||
|
dryRun=
|
||||||
|
keepNamespace=
|
||||||
|
while true; do
|
||||||
|
flag=$1
|
||||||
|
shift
|
||||||
|
case "$flag" in
|
||||||
|
## Next line was modified by nginxproxy/acme-companion
|
||||||
|
--dry-run|-d) dryRun=1 && export DRY_RUN=1 ;;
|
||||||
|
--help|-h|'-?') usage && exit 0 ;;
|
||||||
|
--test|-t) argTests["$1"]=1 && shift ;;
|
||||||
|
--config|-c) configs+=("$(readlink -f "$1")") && shift ;;
|
||||||
|
## Next line was modified by nginxproxy/acme-companion
|
||||||
|
--keep-namespace|-k) keepNamespace=1 ;;
|
||||||
|
--) break ;;
|
||||||
|
*)
|
||||||
|
{
|
||||||
|
echo "error: unknown flag: $flag"
|
||||||
|
usage
|
||||||
|
} >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# declare configuration variables
|
||||||
|
declare -a globalTests=()
|
||||||
|
declare -A testAlias=()
|
||||||
|
declare -A imageTests=()
|
||||||
|
declare -A globalExcludeTests=()
|
||||||
|
declare -A explicitTests=()
|
||||||
|
|
||||||
|
# if there are no user-specified configs, use the default config
|
||||||
|
if [ ${#configs} -eq 0 ]; then
|
||||||
|
configs+=("$dir/config.sh")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# load the configs
|
||||||
|
declare -A testPaths=()
|
||||||
|
for conf in "${configs[@]}"; do
|
||||||
|
## Next two line were modified by nginxproxy/acme-companion
|
||||||
|
# shellcheck source=./config.sh
|
||||||
|
source "$conf"
|
||||||
|
## End of modifications
|
||||||
|
|
||||||
|
# Determine the full path to any newly-declared tests
|
||||||
|
confDir="$(dirname "$conf")"
|
||||||
|
|
||||||
|
for testName in ${globalTests[@]} ${imageTests[@]}; do
|
||||||
|
[ "${testPaths[$testName]}" ] && continue
|
||||||
|
|
||||||
|
if [ -d "$confDir/tests/$testName" ]; then
|
||||||
|
# Test directory found relative to the conf file
|
||||||
|
testPaths[$testName]="$confDir/tests/$testName"
|
||||||
|
elif [ -d "$dir/tests/$testName" ]; then
|
||||||
|
# Test directory found in the main tests/ directory
|
||||||
|
testPaths[$testName]="$dir/tests/$testName"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
didFail=
|
||||||
|
for dockerImage in "$@"; do
|
||||||
|
echo "testing $dockerImage"
|
||||||
|
|
||||||
|
if ! docker inspect "$dockerImage" &> /dev/null; then
|
||||||
|
echo $'\timage does not exist!'
|
||||||
|
didFail=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
repo="${dockerImage%:*}"
|
||||||
|
tagVar="${dockerImage#*:}"
|
||||||
|
#version="${tagVar%-*}"
|
||||||
|
variant="${tagVar##*-}"
|
||||||
|
|
||||||
|
## Irrelevant code was removed here by nginxproxy/acme-companion
|
||||||
|
|
||||||
|
testRepo="$repo"
|
||||||
|
if [ -z "$keepNamespace" ]; then
|
||||||
|
testRepo="${testRepo##*/}"
|
||||||
|
fi
|
||||||
|
[ -z "${testAlias[$repo]}" ] || testRepo="${testAlias[$repo]}"
|
||||||
|
|
||||||
|
explicitVariant=
|
||||||
|
## Next four lines were modified by nginxproxy/acme-companion
|
||||||
|
if [ "${explicitTests[:$variant]}" ] \
|
||||||
|
|| [ "${explicitTests[$repo:$variant]}" ] \
|
||||||
|
|| [ "${explicitTests[$testRepo:$variant]}" ]
|
||||||
|
then
|
||||||
|
## End of modified code
|
||||||
|
explicitVariant=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
testCandidates=()
|
||||||
|
if [ -z "$explicitVariant" ]; then
|
||||||
|
testCandidates+=( "${globalTests[@]}" )
|
||||||
|
fi
|
||||||
|
testCandidates+=(
|
||||||
|
${imageTests[:$variant]}
|
||||||
|
)
|
||||||
|
if [ -z "$explicitVariant" ]; then
|
||||||
|
testCandidates+=(
|
||||||
|
${imageTests[$testRepo]}
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
testCandidates+=(
|
||||||
|
${imageTests[$testRepo:$variant]}
|
||||||
|
)
|
||||||
|
if [ "$testRepo" != "$repo" ]; then
|
||||||
|
if [ -z "$explicitVariant" ]; then
|
||||||
|
testCandidates+=(
|
||||||
|
${imageTests[$repo]}
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
testCandidates+=(
|
||||||
|
${imageTests[$repo:$variant]}
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
tests=()
|
||||||
|
for t in "${testCandidates[@]}"; do
|
||||||
|
## Next line was modified by nginxproxy/acme-companion
|
||||||
|
if [ ${#argTests[@]} -gt 0 ] && [ -z "${argTests[$t]}" ]; then
|
||||||
|
## End of modified code
|
||||||
|
# skipping due to -t
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
## Next seven lines were modified by nginxproxy/acme-companion
|
||||||
|
if [ -n "${globalExcludeTests[${testRepo}_$t]}" ] \
|
||||||
|
|| [ -n "${globalExcludeTests[${testRepo}:${variant}_$t]}" ] \
|
||||||
|
|| [ -n "${globalExcludeTests[:${variant}_$t]}" ] \
|
||||||
|
|| [ -n "${globalExcludeTests[${repo}_$t]}" ] \
|
||||||
|
|| [ -n "${globalExcludeTests[${repo}:${variant}_$t]}" ] \
|
||||||
|
|| [ -n "${globalExcludeTests[:${variant}_$t]}" ]
|
||||||
|
then
|
||||||
|
## End of modified code
|
||||||
|
# skipping due to exclude
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
tests+=( "$t" )
|
||||||
|
done
|
||||||
|
|
||||||
|
currentTest=0
|
||||||
|
totalTest="${#tests[@]}"
|
||||||
|
for t in "${tests[@]}"; do
|
||||||
|
(( currentTest+=1 ))
|
||||||
|
echo -ne "\t'$t' [$currentTest/$totalTest]..."
|
||||||
|
|
||||||
|
# run test against dockerImage here
|
||||||
|
# find the script for the test
|
||||||
|
scriptDir="${testPaths[$t]}"
|
||||||
|
if [ -d "$scriptDir" ]; then
|
||||||
|
script="$scriptDir/run.sh"
|
||||||
|
## Next nine lines were modified or added by nginxproxy/acme-companion
|
||||||
|
if [ -x "$script" ] && [ ! -d "$script" ]; then
|
||||||
|
if [ $dryRun ]; then
|
||||||
|
if "$script" "$dockerImage"; then
|
||||||
|
echo 'passed'
|
||||||
|
else
|
||||||
|
echo 'failed'
|
||||||
|
didFail=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
## End of modified / additional code
|
||||||
|
if output="$("$script" "$dockerImage")"; then
|
||||||
|
if [ -f "$scriptDir/expected-std-out.txt" ] && ! d="$(echo "$output" | diff -u "$scriptDir/expected-std-out.txt" - 2>/dev/null)"; then
|
||||||
|
echo 'failed; unexpected output:'
|
||||||
|
echo "$d"
|
||||||
|
## Next line was added by nginxproxy/acme-companion
|
||||||
|
failed_tests+=("$(basename "$scriptDir")")
|
||||||
|
## End of additional code
|
||||||
|
didFail=1
|
||||||
|
else
|
||||||
|
echo 'passed'
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo 'failed'
|
||||||
|
## Next line was added by nginxproxy/acme-companion
|
||||||
|
failed_tests+=("$(basename "$scriptDir")")
|
||||||
|
## End of additional code
|
||||||
|
didFail=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "skipping"
|
||||||
|
echo >&2 "error: $script missing, not executable or is a directory"
|
||||||
|
didFail=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "skipping"
|
||||||
|
echo >&2 "error: unable to locate test '$t'"
|
||||||
|
didFail=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$didFail" ]; then
|
||||||
|
## Next five lines were added by nginxproxy/acme-companion
|
||||||
|
if [[ $GITHUB_ACTIONS == 'true' ]]; then
|
||||||
|
for test in "${failed_tests[@]}"; do
|
||||||
|
echo "$test" >> "$dir/github_actions/failed_tests.txt"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
## End of additional code
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
12
test/setup/pebble-config.json
Normal file
12
test/setup/pebble-config.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"pebble": {
|
||||||
|
"listenAddress": "0.0.0.0:14000",
|
||||||
|
"managementListenAddress": "0.0.0.0:15000",
|
||||||
|
"certificate": "test/certs/localhost/cert.pem",
|
||||||
|
"privateKey": "test/certs/localhost/key.pem",
|
||||||
|
"httpPort": 80,
|
||||||
|
"tlsPort": 443,
|
||||||
|
"ocspResponderURL": "",
|
||||||
|
"externalAccountBindingRequired": false
|
||||||
|
}
|
||||||
|
}
|
||||||
55
test/setup/setup-boulder.sh
Executable file
55
test/setup/setup-boulder.sh
Executable file
@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
acme_endpoint='http://boulder:4001/directory'
|
||||||
|
|
||||||
|
setup_boulder() {
|
||||||
|
export GOPATH=${GITHUB_WORKSPACE}/go
|
||||||
|
[[ ! -d $GOPATH/src/github.com/letsencrypt/boulder ]] \
|
||||||
|
&& git clone https://github.com/letsencrypt/boulder \
|
||||||
|
"$GOPATH/src/github.com/letsencrypt/boulder"
|
||||||
|
pushd "$GOPATH/src/github.com/letsencrypt/boulder"
|
||||||
|
git checkout release-2020-12-14
|
||||||
|
if [[ "$(uname)" == 'Darwin' ]]; then
|
||||||
|
# Set Standard Ports
|
||||||
|
for file in test/config/va.json test/config/va-remote-a.json test/config/va-remote-b.json; do
|
||||||
|
sed -i '' 's/ 5002/ 80/g' "$file"
|
||||||
|
sed -i '' 's/ 5001/ 443/g' "$file"
|
||||||
|
done
|
||||||
|
# Modify custom rate limit
|
||||||
|
sed -i '' 's/le.wtf,le1.wtf/le1.wtf,le2.wtf,le3.wtf/g' test/rate-limit-policies.yml
|
||||||
|
else
|
||||||
|
# Set Standard Ports
|
||||||
|
for file in test/config/va.json test/config/va-remote-a.json test/config/va-remote-b.json; do
|
||||||
|
sed --in-place 's/ 5002/ 80/g' "$file"
|
||||||
|
sed --in-place 's/ 5001/ 443/g' "$file"
|
||||||
|
done
|
||||||
|
# Modify custom rate limit
|
||||||
|
sed --in-place 's/le.wtf,le1.wtf/le1.wtf,le2.wtf,le3.wtf/g' test/rate-limit-policies.yml
|
||||||
|
fi
|
||||||
|
docker-compose build --pull
|
||||||
|
docker-compose run -d \
|
||||||
|
--use-aliases \
|
||||||
|
--name boulder \
|
||||||
|
-e FAKE_DNS=10.77.77.1 \
|
||||||
|
--service-ports \
|
||||||
|
boulder
|
||||||
|
popd
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_boulder() {
|
||||||
|
i=0
|
||||||
|
until docker exec boulder bash -c "curl ${acme_endpoint:?} >/dev/null 2>&1"; do
|
||||||
|
if [ $i -gt 300 ]; then
|
||||||
|
echo "Boulder has not started for 5 minutes, timing out."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
i=$((i + 5))
|
||||||
|
echo "$acme_endpoint : connection refused, Boulder isn't ready yet. Waiting."
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_boulder
|
||||||
|
wait_for_boulder
|
||||||
169
test/setup/setup-local.sh
Executable file
169
test/setup/setup-local.sh
Executable file
@ -0,0 +1,169 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
function get_environment {
|
||||||
|
dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
|
||||||
|
LOCAL_BUILD_DIR="$(cd "$dir/../.." && pwd)"
|
||||||
|
export GITHUB_WORKSPACE="$LOCAL_BUILD_DIR"
|
||||||
|
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
[[ -f "${GITHUB_WORKSPACE}/test/local_test_env.sh" ]] && \
|
||||||
|
source "${GITHUB_WORKSPACE}/test/local_test_env.sh"
|
||||||
|
|
||||||
|
# Get the environment variables from the .github/workflows/test.yml file with sed
|
||||||
|
declare -a ci_test_yml
|
||||||
|
ci_test_yml[0]="$(sed -n 's/.* NGINX_CONTAINER_NAME: //p' "$LOCAL_BUILD_DIR/.github/workflows/test.yml")"
|
||||||
|
ci_test_yml[1]="$(sed -n 's/.* DOCKER_GEN_CONTAINER_NAME: //p' "$LOCAL_BUILD_DIR/.github/workflows/test.yml")"
|
||||||
|
ci_test_yml[2]="$(sed -n 's/.* TEST_DOMAINS: //p' "$LOCAL_BUILD_DIR/.github/workflows/test.yml")"
|
||||||
|
|
||||||
|
# If environment variable where sourced or manually set use them, else use those from
|
||||||
|
# .github/workflows/test.yml
|
||||||
|
export NGINX_CONTAINER_NAME="${NGINX_CONTAINER_NAME:-${ci_test_yml[0]}}"
|
||||||
|
export DOCKER_GEN_CONTAINER_NAME="${DOCKER_GEN_CONTAINER_NAME:-${ci_test_yml[1]}}"
|
||||||
|
export TEST_DOMAINS="${TEST_DOMAINS:-${ci_test_yml[2]}}"
|
||||||
|
|
||||||
|
# Build the array containing domains to add to /etc/hosts
|
||||||
|
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"
|
||||||
|
|
||||||
|
if [[ -z $SETUP ]]; then
|
||||||
|
while true; do
|
||||||
|
echo "Which nginx-proxy setup do you want to test or remove ?"
|
||||||
|
echo ""
|
||||||
|
echo " 1) Two containers setup (nginx-proxy + le-companion)"
|
||||||
|
echo " 2) Three containers setup (nginx + docker-gen + le-companion)"
|
||||||
|
read -re -p "Select an option [1-2]: " option
|
||||||
|
case $option in
|
||||||
|
1)
|
||||||
|
setup="2containers"
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
setup="3containers"
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
:
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
export SETUP="${SETUP:-$setup}"
|
||||||
|
|
||||||
|
if [[ -z $ACME_CA ]]; then
|
||||||
|
while true; do
|
||||||
|
echo "Which ACME CA do you want to use or remove ?"
|
||||||
|
echo ""
|
||||||
|
echo " 1) Boulder https://github.com/letsencrypt/boulder"
|
||||||
|
echo " 2) Pebble https://github.com/letsencrypt/pebble"
|
||||||
|
read -re -p "Select an option [1-2]: " option
|
||||||
|
case $option in
|
||||||
|
1)
|
||||||
|
acme_ca="boulder"
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
acme_ca="pebble"
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
:
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
export ACME_CA="${ACME_CA:-$acme_ca}"
|
||||||
|
}
|
||||||
|
|
||||||
|
case $1 in
|
||||||
|
--setup)
|
||||||
|
get_environment
|
||||||
|
|
||||||
|
# Prepare the env file that run.sh will source
|
||||||
|
cat > "${GITHUB_WORKSPACE}/test/local_test_env.sh" <<EOF
|
||||||
|
export GITHUB_WORKSPACE="$LOCAL_BUILD_DIR"
|
||||||
|
export NGINX_CONTAINER_NAME="$NGINX_CONTAINER_NAME"
|
||||||
|
export DOCKER_GEN_CONTAINER_NAME="$DOCKER_GEN_CONTAINER_NAME"
|
||||||
|
export TEST_DOMAINS="$TEST_DOMAINS"
|
||||||
|
export SETUP="$SETUP"
|
||||||
|
export ACME_CA="$ACME_CA"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Add the required custom entries to /etc/hosts
|
||||||
|
echo "Adding custom entries to /etc/hosts (requires sudo)."
|
||||||
|
declare -a hosts=("${domains[@]}")
|
||||||
|
if [[ "$ACME_CA" == 'pebble' ]]; then
|
||||||
|
hosts+=(pebble pebble-challtestsrv)
|
||||||
|
fi
|
||||||
|
for host in "${hosts[@]}"; do
|
||||||
|
grep -q "127.0.0.1 $host # le-companion test suite" /etc/hosts \
|
||||||
|
|| echo "127.0.0.1 $host # le-companion test suite" \
|
||||||
|
| sudo tee -a /etc/hosts
|
||||||
|
done
|
||||||
|
|
||||||
|
# Pull nginx:alpine
|
||||||
|
docker pull nginx:alpine
|
||||||
|
|
||||||
|
# Prepare the test setup using the setup scripts
|
||||||
|
if [[ "$ACME_CA" == 'boulder' ]]; then
|
||||||
|
"${GITHUB_WORKSPACE}/test/setup/setup-boulder.sh"
|
||||||
|
elif [[ "$ACME_CA" == 'pebble' ]]; then
|
||||||
|
"${GITHUB_WORKSPACE}/test/setup/setup-pebble.sh"
|
||||||
|
else
|
||||||
|
echo "ACME_CA is not set, aborting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
"${GITHUB_WORKSPACE}/test/setup/setup-nginx-proxy.sh"
|
||||||
|
;;
|
||||||
|
|
||||||
|
--teardown)
|
||||||
|
get_environment
|
||||||
|
|
||||||
|
# Stop and remove nginx-proxy and (if required) docker-gen
|
||||||
|
for cid in $(docker ps -a --filter "label=com.github.jrcs.letsencrypt_nginx_proxy_companion.test_suite" --format "{{.ID}}"); do
|
||||||
|
docker stop "$cid"
|
||||||
|
docker rm --volumes "$cid"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$ACME_CA" == 'boulder' ]]; then
|
||||||
|
# Stop and remove boulder
|
||||||
|
docker-compose --project-name 'boulder' \
|
||||||
|
--file "${GITHUB_WORKSPACE}/go/src/github.com/letsencrypt/boulder/docker-compose.yml" \
|
||||||
|
down --volumes
|
||||||
|
elif [[ "$ACME_CA" == 'pebble' ]]; then
|
||||||
|
docker network rm acme_net
|
||||||
|
[[ -f "${GITHUB_WORKSPACE}/pebble.minica.pem" ]] && rm "${GITHUB_WORKSPACE}/pebble.minica.pem"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cleanup files created by the setup
|
||||||
|
if [[ -n "${GITHUB_WORKSPACE// }" ]]; then
|
||||||
|
[[ -f "${GITHUB_WORKSPACE}/nginx.tmpl" ]] && rm "${GITHUB_WORKSPACE}/nginx.tmpl"
|
||||||
|
rm "${GITHUB_WORKSPACE}/test/local_test_env.sh"
|
||||||
|
echo "The ${GITHUB_WORKSPACE}/go folder require superuser permission to fully remove."
|
||||||
|
echo "Doing sudo rm -rf in scripts is dangerous, so the folder won't be automatically removed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove custom entries to /etc/hosts
|
||||||
|
echo "Removing custom entries from /etc/hosts (requires sudo)."
|
||||||
|
declare -a hosts=("${domains[@]}")
|
||||||
|
if [[ "$ACME_CA" == 'pebble' ]]; then
|
||||||
|
hosts+=(pebble pebble-challtestsrv)
|
||||||
|
fi
|
||||||
|
for host in "${hosts[@]}"; do
|
||||||
|
if [[ "$(uname)" == 'Darwin' ]]; then
|
||||||
|
sudo sed -i '' "/127\.0\.0\.1 $host # le-companion test suite/d" /etc/hosts
|
||||||
|
else
|
||||||
|
sudo sed --in-place "/127\.0\.0\.1 $host # le-companion test suite/d" /etc/hosts
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "Usage:"
|
||||||
|
echo ""
|
||||||
|
echo " --setup : setup the test suite."
|
||||||
|
echo " --teardown : remove the test suite containers, configuration and files."
|
||||||
|
echo ""
|
||||||
|
;;
|
||||||
|
esac
|
||||||
64
test/setup/setup-nginx-proxy.sh
Executable file
64
test/setup/setup-nginx-proxy.sh
Executable file
@ -0,0 +1,64 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
case $ACME_CA in
|
||||||
|
|
||||||
|
pebble)
|
||||||
|
test_net='acme_net'
|
||||||
|
;;
|
||||||
|
|
||||||
|
boulder)
|
||||||
|
test_net='boulder_bluenet'
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "$0 $ACME_CA: invalid option."
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
esac
|
||||||
|
|
||||||
|
case $SETUP in
|
||||||
|
|
||||||
|
2containers)
|
||||||
|
docker run -d -p 80:80 -p 443:443 \
|
||||||
|
--name "$NGINX_CONTAINER_NAME" \
|
||||||
|
--env "DHPARAM_BITS=256" \
|
||||||
|
-v /etc/nginx/vhost.d \
|
||||||
|
-v /etc/nginx/conf.d \
|
||||||
|
-v /usr/share/nginx/html \
|
||||||
|
-v /var/run/docker.sock:/tmp/docker.sock:ro \
|
||||||
|
--label com.github.jrcs.letsencrypt_nginx_proxy_companion.test_suite \
|
||||||
|
--network "$test_net" \
|
||||||
|
nginxproxy/nginx-proxy
|
||||||
|
;;
|
||||||
|
|
||||||
|
3containers)
|
||||||
|
curl https://raw.githubusercontent.com/nginx-proxy/nginx-proxy/main/nginx.tmpl > "${GITHUB_WORKSPACE}/nginx.tmpl"
|
||||||
|
|
||||||
|
docker run -d -p 80:80 -p 443:443 \
|
||||||
|
--name "$NGINX_CONTAINER_NAME" \
|
||||||
|
-v /etc/nginx/conf.d \
|
||||||
|
-v /etc/nginx/certs \
|
||||||
|
-v /etc/nginx/vhost.d \
|
||||||
|
-v /usr/share/nginx/html \
|
||||||
|
--label com.github.jrcs.letsencrypt_nginx_proxy_companion.test_suite \
|
||||||
|
--network "$test_net" \
|
||||||
|
nginx:alpine
|
||||||
|
|
||||||
|
docker run -d \
|
||||||
|
--name "$DOCKER_GEN_CONTAINER_NAME" \
|
||||||
|
--volumes-from "$NGINX_CONTAINER_NAME" \
|
||||||
|
-v "${GITHUB_WORKSPACE}/nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro" \
|
||||||
|
-v /var/run/docker.sock:/tmp/docker.sock:ro \
|
||||||
|
--label com.github.jrcs.letsencrypt_nginx_proxy_companion.test_suite \
|
||||||
|
--network "$test_net" \
|
||||||
|
nginxproxy/docker-gen \
|
||||||
|
-notify-sighup "$NGINX_CONTAINER_NAME" -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "$0 $SETUP: invalid option."
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
esac
|
||||||
51
test/setup/setup-pebble.sh
Executable file
51
test/setup/setup-pebble.sh
Executable file
@ -0,0 +1,51 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
setup_pebble() {
|
||||||
|
docker network create --driver=bridge --subnet=10.30.50.0/24 acme_net
|
||||||
|
curl https://raw.githubusercontent.com/letsencrypt/pebble/master/test/certs/pebble.minica.pem > "${GITHUB_WORKSPACE}/pebble.minica.pem"
|
||||||
|
cat "${GITHUB_WORKSPACE}/pebble.minica.pem"
|
||||||
|
|
||||||
|
docker run -d \
|
||||||
|
--name pebble \
|
||||||
|
--volume "${GITHUB_WORKSPACE}/test/setup/pebble-config.json:/test/config/pebble-config.json" \
|
||||||
|
--network acme_net \
|
||||||
|
--ip="10.30.50.2" \
|
||||||
|
--publish 14000:14000 \
|
||||||
|
--label com.github.jrcs.letsencrypt_nginx_proxy_companion.test_suite \
|
||||||
|
letsencrypt/pebble:v2.3.1 \
|
||||||
|
pebble -config /test/config/pebble-config.json -dnsserver 10.30.50.3:8053
|
||||||
|
|
||||||
|
docker run -d \
|
||||||
|
--name challtestserv \
|
||||||
|
--network acme_net \
|
||||||
|
--ip="10.30.50.3" \
|
||||||
|
--publish 8055:8055 \
|
||||||
|
--label com.github.jrcs.letsencrypt_nginx_proxy_companion.test_suite \
|
||||||
|
letsencrypt/pebble-challtestsrv:v2.3.1 \
|
||||||
|
pebble-challtestsrv -tlsalpn01 ""
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_pebble() {
|
||||||
|
for endpoint in 'https://pebble:14000/dir' 'http://pebble-challtestsrv:8055'; do
|
||||||
|
while ! curl -k "$endpoint" >/dev/null 2>&1; do
|
||||||
|
if [ $((i * 5)) -gt $((5 * 60)) ]; then
|
||||||
|
echo "$endpoint was not available under 5 minutes, timing out."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
i=$((i + 1))
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_pebble_challtestserv() {
|
||||||
|
curl -X POST -d '{"ip":"10.30.50.1"}' http://pebble-challtestsrv:8055/set-default-ipv4
|
||||||
|
curl -X POST -d '{"ip":""}' http://pebble-challtestsrv:8055/set-default-ipv6
|
||||||
|
curl -X POST -d '{"host":"lim.it", "addresses":["10.0.0.0"]}' http://pebble-challtestsrv:8055/add-a
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_pebble
|
||||||
|
wait_for_pebble
|
||||||
|
setup_pebble_challtestserv
|
||||||
1
test/tests/acme_accounts/expected-std-out.txt
Normal file
1
test/tests/acme_accounts/expected-std-out.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
97
test/tests/acme_accounts/run.sh
Executable file
97
test/tests/acme_accounts/run.sh
Executable file
@ -0,0 +1,97 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
## Test for ACME accounts handling.
|
||||||
|
|
||||||
|
if [[ -z $GITHUB_ACTIONS ]]; then
|
||||||
|
le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")"
|
||||||
|
else
|
||||||
|
le_container_name="$(basename "${0%/*}")"
|
||||||
|
fi
|
||||||
|
run_le_container "${1:?}" "$le_container_name"
|
||||||
|
|
||||||
|
# Create the $domains array from comma separated domains in TEST_DOMAINS.
|
||||||
|
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"
|
||||||
|
|
||||||
|
# Cleanup function with EXIT trap
|
||||||
|
function cleanup {
|
||||||
|
# Remove any remaining Nginx container(s) silently.
|
||||||
|
for domain in "${domains[@]}"; do
|
||||||
|
docker rm --force "$domain" &> /dev/null
|
||||||
|
done
|
||||||
|
# Cleanup the files created by this run of the test to avoid foiling following test(s).
|
||||||
|
docker exec "$le_container_name" /app/cleanup_test_artifacts
|
||||||
|
# Stop the LE container
|
||||||
|
docker stop "$le_container_name" > /dev/null
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Run an nginx container for ${domains[0]}.
|
||||||
|
run_nginx_container --hosts "${domains[0]}"
|
||||||
|
|
||||||
|
# Wait for a symlink at /etc/nginx/certs/${domains[0]}.crt
|
||||||
|
wait_for_symlink "${domains[0]}" "$le_container_name"
|
||||||
|
|
||||||
|
# Test if the expected folder / file / content are there.
|
||||||
|
json_file="/etc/acme.sh/default/ca/$ACME_CA/account.json"
|
||||||
|
if [[ "$ACME_CA" == 'boulder' ]]; then
|
||||||
|
no_mail_str='[]'
|
||||||
|
elif [[ "$ACME_CA" == 'pebble' ]]; then
|
||||||
|
no_mail_str='null'
|
||||||
|
fi
|
||||||
|
if docker exec "$le_container_name" [[ ! -d "/etc/acme.sh/default" ]]; then
|
||||||
|
echo "The /etc/acme.sh/default folder does not exist."
|
||||||
|
elif docker exec "$le_container_name" [[ ! -f "$json_file" ]]; then
|
||||||
|
echo "The $json_file file does not exist."
|
||||||
|
elif [[ "$(docker exec "$le_container_name" jq .contact "$json_file")" != "$no_mail_str" ]]; then
|
||||||
|
echo "There is an address set on ${json_file}."
|
||||||
|
docker exec "$le_container_name" jq . "$json_file"
|
||||||
|
docker exec "$le_container_name" jq .contact "$json_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stop the nginx and companion containers silently.
|
||||||
|
docker stop "${domains[0]}" &> /dev/null
|
||||||
|
docker stop "$le_container_name" &> /dev/null
|
||||||
|
|
||||||
|
# Run the companion container with the DEFAULT_EMAIL env var set.
|
||||||
|
default_email="contact@${domains[1]}"
|
||||||
|
le_container_name="${le_container_name}_default"
|
||||||
|
run_le_container "${1:?}" "$le_container_name" "--env DEFAULT_EMAIL=${default_email}"
|
||||||
|
|
||||||
|
# Run an nginx container for ${domains[1]} without LETSENCRYPT_EMAIL set.
|
||||||
|
run_nginx_container --hosts "${domains[1]}"
|
||||||
|
|
||||||
|
# Wait for a symlink at /etc/nginx/certs/${domains[1]}.crt
|
||||||
|
wait_for_symlink "${domains[1]}" "$le_container_name"
|
||||||
|
|
||||||
|
# Test if the expected folder / file / content are there.
|
||||||
|
json_file="/etc/acme.sh/${default_email}/ca/$ACME_CA/account.json"
|
||||||
|
if docker exec "$le_container_name" [[ ! -d "/etc/acme.sh/$default_email" ]]; then
|
||||||
|
echo "The /etc/acme.sh/$default_email folder does not exist."
|
||||||
|
elif docker exec "$le_container_name" [[ ! -f "$json_file" ]]; then
|
||||||
|
echo "The $json_file file does not exist."
|
||||||
|
elif [[ "$(docker exec "$le_container_name" jq -r '.contact|.[0]' "$json_file")" != "mailto:${default_email}" ]]; then
|
||||||
|
echo "$default_email is not set on ${json_file}."
|
||||||
|
docker exec "$le_container_name" jq . "$json_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run an nginx container for ${domains[2]} with LETSENCRYPT_EMAIL set.
|
||||||
|
container_email="contact@${domains[2]}"
|
||||||
|
run_nginx_container --hosts "${domains[2]}" --cli-args "--env LETSENCRYPT_EMAIL=${container_email}"
|
||||||
|
|
||||||
|
# Wait for a symlink at /etc/nginx/certs/${domains[2]}.crt
|
||||||
|
wait_for_symlink "${domains[2]}" "$le_container_name"
|
||||||
|
|
||||||
|
# Test if the expected folder / file / content are there.
|
||||||
|
json_file="/etc/acme.sh/${container_email}/ca/$ACME_CA/account.json"
|
||||||
|
if docker exec "$le_container_name" [[ ! -d "/etc/acme.sh/$container_email" ]]; then
|
||||||
|
echo "The /etc/acme.sh/$container_email folder does not exist."
|
||||||
|
elif docker exec "$le_container_name" [[ ! -f "$json_file" ]]; then
|
||||||
|
echo "The $json_file file does not exist."
|
||||||
|
elif [[ "$(docker exec "$le_container_name" jq -r '.contact|.[0]' "$json_file")" != "mailto:${container_email}" ]]; then
|
||||||
|
echo "$default_email is not set on ${json_file}."
|
||||||
|
docker exec "$le_container_name" jq . "$json_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stop the nginx containers silently.
|
||||||
|
docker stop "${domains[1]}" &> /dev/null
|
||||||
|
docker stop "${domains[2]}" &> /dev/null
|
||||||
1
test/tests/certs_san/expected-std-out.txt
Normal file
1
test/tests/certs_san/expected-std-out.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
96
test/tests/certs_san/run.sh
Executable file
96
test/tests/certs_san/run.sh
Executable file
@ -0,0 +1,96 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
## Test for SAN (Subject Alternative Names) certificates.
|
||||||
|
|
||||||
|
if [[ -z $GITHUB_ACTIONS ]]; then
|
||||||
|
le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")"
|
||||||
|
else
|
||||||
|
le_container_name="$(basename "${0%/*}")"
|
||||||
|
fi
|
||||||
|
run_le_container "${1:?}" "$le_container_name"
|
||||||
|
|
||||||
|
# Create the $domains array from comma separated domains in TEST_DOMAINS.
|
||||||
|
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"
|
||||||
|
|
||||||
|
# Cleanup function with EXIT trap
|
||||||
|
function cleanup {
|
||||||
|
# Remove any remaining Nginx container(s) silently.
|
||||||
|
i=1
|
||||||
|
for hosts in "${letsencrypt_hosts[@]}"; do
|
||||||
|
docker rm --force "test$i" &> /dev/null
|
||||||
|
i=$(( i + 1 ))
|
||||||
|
done
|
||||||
|
# Cleanup the files created by this run of the test to avoid foiling following test(s).
|
||||||
|
docker exec "$le_container_name" /app/cleanup_test_artifacts
|
||||||
|
# Stop the LE container
|
||||||
|
docker stop "$le_container_name" > /dev/null
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Create three different comma separated list from the first three domains in $domains.
|
||||||
|
# testing for regression on spaced lists https://github.com/nginx-proxy/acme-companion/issues/288
|
||||||
|
# with trailing comma https://github.com/nginx-proxy/acme-companion/issues/254
|
||||||
|
# and with trailing dot https://github.com/nginx-proxy/acme-companion/issues/676
|
||||||
|
letsencrypt_hosts=( \
|
||||||
|
[0]="${domains[0]},${domains[1]},${domains[2]}" \ #straight comma separated list
|
||||||
|
[1]="${domains[1]}, ${domains[2]}, ${domains[0]}" \ #comma separated list with spaces
|
||||||
|
[2]="${domains[2]}, ${domains[0]}, ${domains[1]}," \ #comma separated list with spaces and a trailing comma
|
||||||
|
[3]="${domains[0]}.,${domains[2]}.,${domains[1]}" ) #trailing dots
|
||||||
|
|
||||||
|
i=1
|
||||||
|
|
||||||
|
for hosts in "${letsencrypt_hosts[@]}"; do
|
||||||
|
|
||||||
|
# Get the base domain (first domain of the list).
|
||||||
|
base_domain="$(get_base_domain "$hosts")"
|
||||||
|
container="test$i"
|
||||||
|
|
||||||
|
# Run an Nginx container passing one of the comma separated list as LETSENCRYPT_HOST env var.
|
||||||
|
run_nginx_container --hosts "$hosts" --name "$container"
|
||||||
|
|
||||||
|
# Wait for a symlink at /etc/nginx/certs/$base_domain.crt
|
||||||
|
if wait_for_symlink "$base_domain" "$le_container_name" "./${base_domain}/fullchain.pem"; then
|
||||||
|
# then grab the certificate in text form ...
|
||||||
|
created_cert="$(docker exec "$le_container_name" \
|
||||||
|
openssl x509 -in "/etc/nginx/certs/${base_domain}/cert.pem" -text -noout)"
|
||||||
|
# ... as well as the certificate fingerprint.
|
||||||
|
created_cert_fingerprint="$(docker exec "$le_container_name" \
|
||||||
|
openssl x509 -in "/etc/nginx/certs/${base_domain}/cert.pem" -fingerprint -noout)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for domain in "${domains[@]}"; do
|
||||||
|
## For all the domains in the $domains array ...
|
||||||
|
|
||||||
|
# Check if the domain is on the certificate.
|
||||||
|
if ! grep -q "$domain" <<< "$created_cert"; then
|
||||||
|
echo "$domain did not appear on certificate."
|
||||||
|
elif [[ "${DRY_RUN:-}" == 1 ]]; then
|
||||||
|
echo "$domain is on certificate."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wait for a connection to https://domain then grab the served certificate in text form.
|
||||||
|
wait_for_conn --domain "$domain"
|
||||||
|
served_cert_fingerprint="$(echo \
|
||||||
|
| openssl s_client -showcerts -servername "$domain" -connect "$domain:443" 2>/dev/null \
|
||||||
|
| openssl x509 -fingerprint -noout)"
|
||||||
|
|
||||||
|
|
||||||
|
# Compare the cert on file and what we got from the https connection.
|
||||||
|
# If not identical, display a full diff.
|
||||||
|
if [ "$created_cert_fingerprint" != "$served_cert_fingerprint" ]; then
|
||||||
|
echo "Nginx served an incorrect certificate for $domain."
|
||||||
|
served_cert="$(echo \
|
||||||
|
| openssl s_client -showcerts -servername "$domain" -connect "$domain:443" 2>/dev/null \
|
||||||
|
| openssl x509 -text -noout \
|
||||||
|
| sed 's/ = /=/g' )"
|
||||||
|
diff -u <(echo "${created_cert// = /=}") <(echo "$served_cert")
|
||||||
|
elif [[ "${DRY_RUN:-}" == 1 ]]; then
|
||||||
|
echo "The correct certificate for $domain was served by Nginx."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
docker stop "$container" &> /dev/null
|
||||||
|
docker exec "$le_container_name" /app/cleanup_test_artifacts
|
||||||
|
i=$(( i + 1 ))
|
||||||
|
|
||||||
|
done
|
||||||
1
test/tests/certs_single/expected-std-out.txt
Normal file
1
test/tests/certs_single/expected-std-out.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
75
test/tests/certs_single/run.sh
Executable file
75
test/tests/certs_single/run.sh
Executable file
@ -0,0 +1,75 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
## Test for single domain certificates.
|
||||||
|
|
||||||
|
if [[ -z $GITHUB_ACTIONS ]]; then
|
||||||
|
le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")"
|
||||||
|
else
|
||||||
|
le_container_name="$(basename "${0%/*}")"
|
||||||
|
fi
|
||||||
|
run_le_container "${1:?}" "$le_container_name"
|
||||||
|
|
||||||
|
# Create the $domains array from comma separated domains in TEST_DOMAINS.
|
||||||
|
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"
|
||||||
|
|
||||||
|
# Cleanup function with EXIT trap
|
||||||
|
function cleanup {
|
||||||
|
# Remove any remaining Nginx container(s) silently.
|
||||||
|
for domain in "${domains[@]}"; do
|
||||||
|
docker rm --force "$domain" &> /dev/null
|
||||||
|
done
|
||||||
|
# Cleanup the files created by this run of the test to avoid foiling following test(s).
|
||||||
|
docker exec "$le_container_name" /app/cleanup_test_artifacts
|
||||||
|
# Stop the LE container
|
||||||
|
docker stop "$le_container_name" > /dev/null
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Run a separate nginx container for each domain in the $domains array.
|
||||||
|
# Start all the containers in a row so that docker-gen debounce timers fire only once.
|
||||||
|
for domain in "${domains[@]}"; do
|
||||||
|
run_nginx_container --hosts "$domain"
|
||||||
|
done
|
||||||
|
|
||||||
|
for domain in "${domains[@]}"; do
|
||||||
|
|
||||||
|
# Wait for a symlink at /etc/nginx/certs/$domain.crt
|
||||||
|
if wait_for_symlink "$domain" "$le_container_name" "./${domain}/fullchain.pem" ; then
|
||||||
|
# then grab the certificate in text form from the file ...
|
||||||
|
created_cert="$(docker exec "$le_container_name" \
|
||||||
|
openssl x509 -in "/etc/nginx/certs/${domain}/cert.pem" -text -noout)"
|
||||||
|
# ... as well as the certificate fingerprint.
|
||||||
|
created_cert_fingerprint="$(docker exec "$le_container_name" \
|
||||||
|
openssl x509 -in "/etc/nginx/certs/${domain}/cert.pem" -fingerprint -noout)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Check if the domain is on the certificate.
|
||||||
|
if ! grep -q "$domain" <<< "$created_cert"; then
|
||||||
|
echo "Domain $domain isn't on certificate."
|
||||||
|
elif [[ "${DRY_RUN:-}" == 1 ]]; then
|
||||||
|
echo "Domain $domain is on certificate."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wait for a connection to https://domain then grab the served certificate fingerprint.
|
||||||
|
wait_for_conn --domain "$domain"
|
||||||
|
served_cert_fingerprint="$(echo \
|
||||||
|
| openssl s_client -showcerts -servername "$domain" -connect "$domain:443" 2>/dev/null \
|
||||||
|
| openssl x509 -fingerprint -noout)"
|
||||||
|
|
||||||
|
# Compare fingerprints from the cert on file and what we got from the https connection.
|
||||||
|
# If not identical, display a full diff.
|
||||||
|
if [ "$created_cert_fingerprint" != "$served_cert_fingerprint" ]; then
|
||||||
|
echo "Nginx served an incorrect certificate for $domain."
|
||||||
|
served_cert="$(echo \
|
||||||
|
| openssl s_client -showcerts -servername "$domain" -connect "$domain:443" 2>/dev/null \
|
||||||
|
| openssl x509 -text -noout \
|
||||||
|
| sed 's/ = /=/g' )"
|
||||||
|
diff -u <(echo "${created_cert// = /=}") <(echo "$served_cert")
|
||||||
|
elif [[ "${DRY_RUN:-}" == 1 ]]; then
|
||||||
|
echo "The correct certificate for $domain was served by Nginx."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stop the Nginx container silently.
|
||||||
|
docker stop "$domain" > /dev/null
|
||||||
|
done
|
||||||
1
test/tests/certs_single_domain/expected-std-out.txt
Normal file
1
test/tests/certs_single_domain/expected-std-out.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
102
test/tests/certs_single_domain/run.sh
Executable file
102
test/tests/certs_single_domain/run.sh
Executable file
@ -0,0 +1,102 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
## Test for spliting SAN certificates into single domain certificates by NGINX container env variables
|
||||||
|
|
||||||
|
if [[ -z $GITHUB_ACTIONS ]]; then
|
||||||
|
le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")"
|
||||||
|
else
|
||||||
|
le_container_name="$(basename "${0%/*}")"
|
||||||
|
fi
|
||||||
|
run_le_container "${1:?}" "$le_container_name"
|
||||||
|
|
||||||
|
# Create the $domains array from comma separated domains in TEST_DOMAINS.
|
||||||
|
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"
|
||||||
|
|
||||||
|
# Cleanup function with EXIT trap
|
||||||
|
function cleanup {
|
||||||
|
# Remove any remaining Nginx container(s) silently.
|
||||||
|
i=1
|
||||||
|
for hosts in "${letsencrypt_hosts[@]}"; do
|
||||||
|
docker rm --force "test$i" &> /dev/null
|
||||||
|
i=$(( i + 1 ))
|
||||||
|
done
|
||||||
|
# Cleanup the files created by this run of the test to avoid foiling following test(s).
|
||||||
|
docker exec "$le_container_name" /app/cleanup_test_artifacts
|
||||||
|
# Stop the LE container
|
||||||
|
docker stop "$le_container_name" > /dev/null
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Create three different comma separated list from the first three domains in $domains.
|
||||||
|
# testing for regression on spaced lists https://github.com/nginx-proxy/acme-companion/issues/288
|
||||||
|
# and with trailing comma https://github.com/nginx-proxy/acme-companion/issues/254
|
||||||
|
letsencrypt_hosts=( \
|
||||||
|
[0]="${domains[0]},${domains[1]},${domains[2]}" \ #straight comma separated list
|
||||||
|
[1]="${domains[1]}, ${domains[2]}, ${domains[0]}" \ #comma separated list with spaces
|
||||||
|
[2]="${domains[2]}, ${domains[0]}, ${domains[1]}," ) #comma separated list with spaces and a trailing comma
|
||||||
|
|
||||||
|
i=1
|
||||||
|
|
||||||
|
for hosts in "${letsencrypt_hosts[@]}"; do
|
||||||
|
|
||||||
|
container="test$i"
|
||||||
|
|
||||||
|
# Run an Nginx container passing one of the comma separated list as LETSENCRYPT_HOST env var.
|
||||||
|
run_nginx_container --hosts "${hosts}" --name "$container" --cli-args "--env LETSENCRYPT_SINGLE_DOMAIN_CERTS=true"
|
||||||
|
|
||||||
|
for domain in "${domains[@]}"; do
|
||||||
|
## For all the domains in the $domains array ...
|
||||||
|
# Wait for a symlink at /etc/nginx/certs/$domain.crt
|
||||||
|
if wait_for_symlink "${domain}" "$le_container_name" "./${domain}/fullchain.pem"; then
|
||||||
|
# then grab the certificate in text form from the file ...
|
||||||
|
created_cert="$(docker exec "$le_container_name" \
|
||||||
|
openssl x509 -in "/etc/nginx/certs/${domain}/cert.pem" -text -noout)"
|
||||||
|
# ... as well as the certificate fingerprint.
|
||||||
|
created_cert_fingerprint="$(docker exec "$le_container_name" \
|
||||||
|
openssl x509 -in "/etc/nginx/certs/${domain}/cert.pem" -fingerprint -noout)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the domain is on the certificate.
|
||||||
|
if grep -q "$domain" <<< "$created_cert"; then
|
||||||
|
if [[ "${DRY_RUN:-}" == 1 ]]; then
|
||||||
|
echo "$domain is on certificate."
|
||||||
|
fi
|
||||||
|
for otherdomain in "${domains[@]}"; do
|
||||||
|
if [ "$domain" != "$otherdomain" ]; then
|
||||||
|
if grep -q "$otherdomain" <<< "$created_cert"; then
|
||||||
|
echo "$otherdomain is on certificate for $domain, but it must not!"
|
||||||
|
elif [[ "${DRY_RUN:-}" == 1 ]]; then
|
||||||
|
echo "$otherdomain did not appear on certificate for $domain."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "$domain did not appear on certificate."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wait for a connection to https://domain then grab the served certificate in text form.
|
||||||
|
wait_for_conn --domain "$domain"
|
||||||
|
served_cert_fingerprint="$(echo \
|
||||||
|
| openssl s_client -showcerts -servername "$domain" -connect "$domain:443" 2>/dev/null \
|
||||||
|
| openssl x509 -fingerprint -noout)"
|
||||||
|
|
||||||
|
|
||||||
|
# Compare the cert on file and what we got from the https connection.
|
||||||
|
# If not identical, display a full diff.
|
||||||
|
if [ "$created_cert_fingerprint" != "$served_cert_fingerprint" ]; then
|
||||||
|
echo "Nginx served an incorrect certificate for $domain."
|
||||||
|
served_cert="$(echo \
|
||||||
|
| openssl s_client -showcerts -servername "$domain" -connect "$domain:443" 2>/dev/null \
|
||||||
|
| openssl x509 -text -noout \
|
||||||
|
| sed 's/ = /=/g' )"
|
||||||
|
diff -u <(echo "${created_cert// = /=}") <(echo "$served_cert")
|
||||||
|
elif [[ "${DRY_RUN:-}" == 1 ]]; then
|
||||||
|
echo "The correct certificate for $domain was served by Nginx."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
docker stop "$container" &> /dev/null
|
||||||
|
docker exec "$le_container_name" /app/cleanup_test_artifacts --default-cert
|
||||||
|
i=$(( i + 1 ))
|
||||||
|
|
||||||
|
done
|
||||||
1
test/tests/certs_standalone/expected-std-out.txt
Normal file
1
test/tests/certs_standalone/expected-std-out.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
112
test/tests/certs_standalone/run.sh
Executable file
112
test/tests/certs_standalone/run.sh
Executable file
@ -0,0 +1,112 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
## Test for standalone certificates.
|
||||||
|
|
||||||
|
case $ACME_CA in
|
||||||
|
pebble)
|
||||||
|
test_net='acme_net'
|
||||||
|
;;
|
||||||
|
boulder)
|
||||||
|
test_net='boulder_bluenet'
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "$0 $ACME_CA: invalid option."
|
||||||
|
exit 1
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ -z $GITHUB_ACTIONS ]]; then
|
||||||
|
le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")"
|
||||||
|
else
|
||||||
|
le_container_name="$(basename "${0%/*}")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create the $domains array from comma separated domains in TEST_DOMAINS.
|
||||||
|
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"
|
||||||
|
subdomain="sub.${domains[0]}"
|
||||||
|
|
||||||
|
# Cleanup function with EXIT trap
|
||||||
|
function cleanup {
|
||||||
|
# Remove the Nginx container silently.
|
||||||
|
docker rm --force "$subdomain" &> /dev/null
|
||||||
|
# Cleanup the files created by this run of the test to avoid foiling following test(s).
|
||||||
|
docker exec "$le_container_name" /app/cleanup_test_artifacts
|
||||||
|
# Stop the LE container
|
||||||
|
docker stop "$le_container_name" > /dev/null
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Create letsencrypt_user_data with a single domain cert
|
||||||
|
cat > "${GITHUB_WORKSPACE}/test/tests/certs_standalone/letsencrypt_user_data" <<EOF
|
||||||
|
LETSENCRYPT_STANDALONE_CERTS=('single')
|
||||||
|
LETSENCRYPT_single_HOST=('${domains[0]}')
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Run an nginx container with a VIRTUAL_HOST set to a subdomain of ${domains[0]} in order to check for
|
||||||
|
# this regression : https://github.com/nginx-proxy/acme-companion/issues/674
|
||||||
|
if ! docker run --rm -d \
|
||||||
|
--name "$subdomain" \
|
||||||
|
-e "VIRTUAL_HOST=$subdomain" \
|
||||||
|
--network "$test_net" \
|
||||||
|
nginx:alpine > /dev/null;
|
||||||
|
then
|
||||||
|
echo "Could not start test web server for $subdomain"
|
||||||
|
elif [[ "${DRY_RUN:-}" == 1 ]]; then
|
||||||
|
echo "Started test web server for $subdomain"
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_le_container "${1:?}" "$le_container_name" \
|
||||||
|
"--volume ${GITHUB_WORKSPACE}/test/tests/certs_standalone/letsencrypt_user_data:/app/letsencrypt_user_data"
|
||||||
|
|
||||||
|
# Wait for a file at /etc/nginx/conf.d/standalone-cert-${domains[0]}.conf
|
||||||
|
wait_for_standalone_conf "${domains[0]}" "$le_container_name"
|
||||||
|
|
||||||
|
# Wait for a symlink at /etc/nginx/certs/${domains[0]}.crt
|
||||||
|
if wait_for_symlink "${domains[0]}" "$le_container_name"; then
|
||||||
|
# then grab the certificate in text form ...
|
||||||
|
created_cert="$(docker exec "$le_container_name" \
|
||||||
|
openssl x509 -in "/etc/nginx/certs/${domains[0]}/cert.pem" -text -noout)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the domain is on the certificate.
|
||||||
|
if ! grep -q "${domains[0]}" <<< "$created_cert"; then
|
||||||
|
echo "Domain ${domains[0]} did not appear on certificate."
|
||||||
|
elif [[ "${DRY_RUN:-}" == 1 ]]; then
|
||||||
|
echo "Domain ${domains[0]} is on certificate."
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker exec "$le_container_name" bash -c "[[ -f /etc/nginx/conf.d/standalone-cert-${domains[0]}.conf ]]" \
|
||||||
|
&& echo "Standalone configuration for ${domains[0]} wasn't correctly removed."
|
||||||
|
|
||||||
|
# Add another (SAN) certificate to letsencrypt_user_data
|
||||||
|
cat > "${GITHUB_WORKSPACE}/test/tests/certs_standalone/letsencrypt_user_data" <<EOF
|
||||||
|
LETSENCRYPT_STANDALONE_CERTS=('single' 'san')
|
||||||
|
LETSENCRYPT_single_HOST=('${domains[0]}')
|
||||||
|
LETSENCRYPT_san_HOST=('${domains[1]}' '${domains[2]}')
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Manually trigger the service loop
|
||||||
|
docker exec "$le_container_name" /app/signal_le_service > /dev/null
|
||||||
|
|
||||||
|
for domain in "${domains[1]}" "${domains[2]}"; do
|
||||||
|
# Wait for a file at /etc/nginx/conf.d/standalone-cert-$domain.conf
|
||||||
|
wait_for_standalone_conf "$domain" "$le_container_name"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Wait for a symlink at /etc/nginx/certs/${domains[1]}.crt
|
||||||
|
if wait_for_symlink "${domains[1]}" "$le_container_name"; then
|
||||||
|
# then grab the certificate in text form ...
|
||||||
|
created_cert="$(docker exec "$le_container_name" \
|
||||||
|
openssl x509 -in "/etc/nginx/certs/${domains[1]}/cert.pem" -text -noout)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for domain in "${domains[1]}" "${domains[2]}"; do
|
||||||
|
# Check if the domain is on the certificate.
|
||||||
|
if ! grep -q "$domain" <<< "$created_cert"; then
|
||||||
|
echo "Domain $domain did not appear on certificate."
|
||||||
|
elif [[ "${DRY_RUN:-}" == 1 ]]; then
|
||||||
|
echo "Domain $domain is on certificate."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
docker exec "$le_container_name" bash -c "[[ ! -f /etc/nginx/conf.d/standalone-cert-${domains[1]}.conf ]]" \
|
||||||
|
|| echo "Standalone configuration for ${domains[1]} wasn't correctly removed."
|
||||||
3
test/tests/container_restart/expected-std-out.txt
Normal file
3
test/tests/container_restart/expected-std-out.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Container le1.wtf restarted
|
||||||
|
Container le2.wtf restarted
|
||||||
|
Container le3.wtf restarted
|
||||||
52
test/tests/container_restart/run.sh
Executable file
52
test/tests/container_restart/run.sh
Executable file
@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
## Test for LETSENCRYPT_RESTART_CONTAINER variable.
|
||||||
|
|
||||||
|
if [[ -z $GITHUB_ACTIONS ]]; then
|
||||||
|
le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")"
|
||||||
|
else
|
||||||
|
le_container_name="$(basename "${0%/*}")"
|
||||||
|
fi
|
||||||
|
run_le_container "${1:?}" "$le_container_name"
|
||||||
|
|
||||||
|
# Create the $domains array from comma separated domains in TEST_DOMAINS.
|
||||||
|
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"
|
||||||
|
|
||||||
|
# Listen for Docker restart events
|
||||||
|
docker events \
|
||||||
|
--filter event=restart \
|
||||||
|
--format 'Container {{.Actor.Attributes.name}} restarted' > "${GITHUB_WORKSPACE}/test/tests/container_restart/docker_event_out.txt" &
|
||||||
|
docker_events_pid=$!
|
||||||
|
|
||||||
|
# Cleanup function with EXIT trap
|
||||||
|
function cleanup {
|
||||||
|
# Kill the Docker events listener
|
||||||
|
kill $docker_events_pid && wait $docker_events_pid 2>/dev/null
|
||||||
|
# Remove temporary files
|
||||||
|
rm -f "${GITHUB_WORKSPACE}/test/tests/container_restart/docker_event_out.txt"
|
||||||
|
# Remove any remaining Nginx container(s) silently.
|
||||||
|
for domain in "${domains[@]}"; do
|
||||||
|
docker rm --force "$domain" &> /dev/null
|
||||||
|
done
|
||||||
|
# Cleanup the files created by this run of the test to avoid foiling following test(s).
|
||||||
|
docker exec "$le_container_name" /app/cleanup_test_artifacts
|
||||||
|
# Stop the LE container
|
||||||
|
docker stop "$le_container_name" > /dev/null
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Run a separate nginx container for each domain in the $domains array.
|
||||||
|
for domain in "${domains[@]}"; do
|
||||||
|
run_nginx_container --hosts "$domain" --cli-args "--env LETSENCRYPT_RESTART_CONTAINER=true"
|
||||||
|
|
||||||
|
# Check if container restarted
|
||||||
|
timeout="$(date +%s)"
|
||||||
|
timeout="$((timeout + 120))"
|
||||||
|
until grep "$domain" "${GITHUB_WORKSPACE}"/test/tests/container_restart/docker_event_out.txt; do
|
||||||
|
if [[ "$(date +%s)" -gt "$timeout" ]]; then
|
||||||
|
echo "Container $domain didn't restart in under one minute."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
done
|
||||||
1
test/tests/default_cert/expected-std-out.txt
Normal file
1
test/tests/default_cert/expected-std-out.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
105
test/tests/default_cert/run.sh
Executable file
105
test/tests/default_cert/run.sh
Executable file
@ -0,0 +1,105 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
## Test for default certificate creation.
|
||||||
|
|
||||||
|
if [[ -z $GITHUB_ACTIONS ]]; then
|
||||||
|
le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")"
|
||||||
|
else
|
||||||
|
le_container_name="$(basename "${0%/*}")"
|
||||||
|
fi
|
||||||
|
run_le_container "${1:?}" "$le_container_name"
|
||||||
|
|
||||||
|
# Create the $domains array from comma separated domains in TEST_DOMAINS.
|
||||||
|
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"
|
||||||
|
|
||||||
|
# Cleanup function with EXIT trap
|
||||||
|
function cleanup {
|
||||||
|
# Cleanup the files created by this run of the test to avoid foiling following test(s).
|
||||||
|
docker exec "$le_container_name" /app/cleanup_test_artifacts --default-cert
|
||||||
|
docker stop "$le_container_name" > /dev/null
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
function check_default_cert_existence {
|
||||||
|
docker exec "$le_container_name" [[ -f "/etc/nginx/certs/default.crt" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
function default_cert_fingerprint {
|
||||||
|
if check_default_cert_existence; then
|
||||||
|
docker exec "$le_container_name" openssl x509 -in "/etc/nginx/certs/default.crt" -fingerprint -noout
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function default_cert_subject {
|
||||||
|
if check_default_cert_existence; then
|
||||||
|
docker exec "$le_container_name" openssl x509 -in "/etc/nginx/certs/default.crt" -subject -noout
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
user_cn="user-provided"
|
||||||
|
|
||||||
|
timeout="$(date +%s)"
|
||||||
|
timeout="$((timeout + 120))"
|
||||||
|
until docker exec "$le_container_name" [[ -f /etc/nginx/certs/default.crt ]]; do
|
||||||
|
if [[ "$(date +%s)" -gt "$timeout" ]]; then
|
||||||
|
echo "Default cert wasn't created under one minute at container first launch."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Connection test to unconfigured domains
|
||||||
|
for domain in "${domains[@]}"; do
|
||||||
|
wait_for_conn --domain "$domain" --default-cert
|
||||||
|
done
|
||||||
|
|
||||||
|
# Test if the default certificate get re-created when
|
||||||
|
# the certificate or private key file are deleted
|
||||||
|
for file in 'default.key' 'default.crt'; do
|
||||||
|
old_default_cert_fingerprint="$(default_cert_fingerprint)"
|
||||||
|
docker exec "$le_container_name" /app/cleanup_test_artifacts --default-cert
|
||||||
|
docker restart "$le_container_name" > /dev/null
|
||||||
|
timeout="$(date +%s)"
|
||||||
|
timeout="$((timeout + 120))"
|
||||||
|
while [[ "$(default_cert_fingerprint)" == "$old_default_cert_fingerprint" ]]; do
|
||||||
|
if [[ "$(date +%s)" -gt "$timeout" ]]; then
|
||||||
|
echo "Default cert wasn't re-created under one minute after $file deletion."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# Test if the default certificate get re-created when
|
||||||
|
# the certificate expire in less than three months
|
||||||
|
docker exec "$le_container_name" bash -c 'rm -rf /etc/nginx/certs/default.*'
|
||||||
|
docker exec "$le_container_name" openssl req -x509 \
|
||||||
|
-newkey rsa:4096 -sha256 -nodes -days 60 \
|
||||||
|
-subj "/CN=letsencrypt-nginx-proxy-companion" \
|
||||||
|
-keyout /etc/nginx/certs/default.key \
|
||||||
|
-out /etc/nginx/certs/default.crt &> /dev/null
|
||||||
|
old_default_cert_fingerprint="$(default_cert_fingerprint)"
|
||||||
|
docker restart "$le_container_name" > /dev/null && sleep 10
|
||||||
|
timeout="$(date +%s)"
|
||||||
|
timeout="$((timeout + 110))"
|
||||||
|
while [[ "$(default_cert_fingerprint)" == "$old_default_cert_fingerprint" ]]; do
|
||||||
|
if [[ "$(date +%s)" -gt "$timeout" ]]; then
|
||||||
|
echo "Default cert wasn't re-created under one minute when the certificate expire in less than three months."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Test that a user provided default certificate isn't overwrited
|
||||||
|
docker exec "$le_container_name" bash -c 'rm -rf /etc/nginx/certs/default.*'
|
||||||
|
docker exec "$le_container_name" openssl req -x509 \
|
||||||
|
-newkey rsa:4096 -sha256 -nodes -days 60 \
|
||||||
|
-subj "/CN=$user_cn" \
|
||||||
|
-keyout /etc/nginx/certs/default.key \
|
||||||
|
-out /etc/nginx/certs/default.crt &> /dev/null
|
||||||
|
docker restart "$le_container_name" > /dev/null
|
||||||
|
|
||||||
|
# Connection test to unconfigured domains
|
||||||
|
for domain in "${domains[@]}"; do
|
||||||
|
wait_for_conn --domain "$domain" --subject-match "$user_cn"
|
||||||
|
done
|
||||||
252
test/tests/docker_api/run.sh
Executable file
252
test/tests/docker_api/run.sh
Executable file
@ -0,0 +1,252 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
## Test for the Docker API.
|
||||||
|
|
||||||
|
nginx_vol='nginx-volumes-from'
|
||||||
|
nginx_env='nginx-env-var'
|
||||||
|
nginx_lbl='nginx-label'
|
||||||
|
docker_gen='docker-gen-no-label'
|
||||||
|
docker_gen_lbl='docker-gen-label'
|
||||||
|
|
||||||
|
case $SETUP in
|
||||||
|
|
||||||
|
2containers)
|
||||||
|
# Cleanup function with EXIT trap
|
||||||
|
function cleanup {
|
||||||
|
# Kill the Docker events listener
|
||||||
|
kill "$docker_events_pid" && wait "$docker_events_pid" 2>/dev/null
|
||||||
|
# Remove the remaining containers silently
|
||||||
|
docker rm --force \
|
||||||
|
"$nginx_vol" \
|
||||||
|
"$nginx_env" \
|
||||||
|
"$nginx_lbl" \
|
||||||
|
&> /dev/null
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Set the commands to be passed to docker exec
|
||||||
|
commands='source /app/functions.sh; reload_nginx > /dev/null; check_nginx_proxy_container_run; get_nginx_proxy_container'
|
||||||
|
|
||||||
|
# Listen to Docker exec_start events
|
||||||
|
docker events \
|
||||||
|
--filter event=exec_start \
|
||||||
|
--format 'Container {{.Actor.Attributes.name}} received {{.Action}}' &
|
||||||
|
docker_events_pid=$!
|
||||||
|
|
||||||
|
# Run a nginx-proxy container named nginx-volumes-from, without the nginx_proxy label
|
||||||
|
docker run --rm -d \
|
||||||
|
--name "$nginx_vol" \
|
||||||
|
-v /var/run/docker.sock:/tmp/docker.sock:ro \
|
||||||
|
nginxproxy/nginx-proxy > /dev/null
|
||||||
|
|
||||||
|
# Run a nginx-proxy container named nginx-env-var, without the nginx_proxy label
|
||||||
|
docker run --rm -d \
|
||||||
|
--name "$nginx_env" \
|
||||||
|
-v /var/run/docker.sock:/tmp/docker.sock:ro \
|
||||||
|
nginxproxy/nginx-proxy > /dev/null
|
||||||
|
|
||||||
|
# This should target the nginx-proxy container obtained with
|
||||||
|
# the --volume-from argument (nginx-volumes-from)
|
||||||
|
docker run --rm \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||||
|
--volumes-from "$nginx_vol" \
|
||||||
|
"$1" \
|
||||||
|
bash -c "$commands" 2>&1
|
||||||
|
|
||||||
|
# This should target the nginx-proxy container obtained with
|
||||||
|
# the NGINX_PROXY_CONTAINER environment variable (nginx-env-var)
|
||||||
|
docker run --rm \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||||
|
--volumes-from "$nginx_vol" \
|
||||||
|
-e "NGINX_PROXY_CONTAINER=$nginx_env" \
|
||||||
|
"$1" \
|
||||||
|
bash -c "$commands" 2>&1
|
||||||
|
|
||||||
|
# Run a nginx-proxy container named nginx-label, with the nginx_proxy label.
|
||||||
|
# Store the container id in the labeled_nginx_cid variable.
|
||||||
|
labeled_nginx_cid="$(docker run --rm -d \
|
||||||
|
--name "$nginx_lbl" \
|
||||||
|
-v /var/run/docker.sock:/tmp/docker.sock:ro \
|
||||||
|
--label com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy \
|
||||||
|
nginxproxy/nginx-proxy)"
|
||||||
|
|
||||||
|
# This should target the nginx-proxy container with the label (nginx-label)
|
||||||
|
docker run --rm \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||||
|
--volumes-from "$nginx_vol" \
|
||||||
|
-e "NGINX_PROXY_CONTAINER=$nginx_env" \
|
||||||
|
"$1" \
|
||||||
|
bash -c "$commands" 2>&1
|
||||||
|
|
||||||
|
cat > "${GITHUB_WORKSPACE}/test/tests/docker_api/expected-std-out.txt" <<EOF
|
||||||
|
Container $nginx_vol received exec_start: sh -c /app/docker-entrypoint.sh /usr/local/bin/docker-gen /app/nginx.tmpl /etc/nginx/conf.d/default.conf; /usr/sbin/nginx -s reload
|
||||||
|
$nginx_vol
|
||||||
|
Container $nginx_env received exec_start: sh -c /app/docker-entrypoint.sh /usr/local/bin/docker-gen /app/nginx.tmpl /etc/nginx/conf.d/default.conf; /usr/sbin/nginx -s reload
|
||||||
|
$nginx_env
|
||||||
|
Container $nginx_lbl received exec_start: sh -c /app/docker-entrypoint.sh /usr/local/bin/docker-gen /app/nginx.tmpl /etc/nginx/conf.d/default.conf; /usr/sbin/nginx -s reload
|
||||||
|
$labeled_nginx_cid
|
||||||
|
EOF
|
||||||
|
;;
|
||||||
|
|
||||||
|
3containers)
|
||||||
|
# Cleanup function with EXIT trap
|
||||||
|
function cleanup {
|
||||||
|
# Kill the Docker events listener
|
||||||
|
kill $docker_events_pid && wait $docker_events_pid 2>/dev/null
|
||||||
|
# Remove the remaining containers silently
|
||||||
|
docker stop \
|
||||||
|
"$nginx_vol" \
|
||||||
|
"$nginx_env" \
|
||||||
|
"$nginx_lbl" \
|
||||||
|
"$docker_gen" \
|
||||||
|
"$docker_gen_lbl" \
|
||||||
|
&> /dev/null
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Set the commands to be passed to docker exec
|
||||||
|
commands='source /app/functions.sh; reload_nginx > /dev/null; check_nginx_proxy_container_run; get_docker_gen_container; get_nginx_proxy_container'
|
||||||
|
|
||||||
|
# Listen to Docker kill events
|
||||||
|
docker events \
|
||||||
|
--filter event=kill \
|
||||||
|
--format 'Container {{.Actor.Attributes.name}} received signal {{.Actor.Attributes.signal}}' &
|
||||||
|
docker_events_pid=$!
|
||||||
|
|
||||||
|
# Run a nginx container named nginx-volumes-from, without the nginx_proxy label.
|
||||||
|
docker run --rm -d \
|
||||||
|
--name "$nginx_vol" \
|
||||||
|
-v /var/run/docker.sock:/tmp/docker.sock:ro \
|
||||||
|
nginx:alpine > /dev/null
|
||||||
|
|
||||||
|
# Run a nginx container named nginx-env-var, without the nginx_proxy label.
|
||||||
|
docker run --rm -d \
|
||||||
|
--name "$nginx_env" \
|
||||||
|
-v /var/run/docker.sock:/tmp/docker.sock:ro \
|
||||||
|
nginx:alpine > /dev/null
|
||||||
|
|
||||||
|
# Spawn a "fake docker-gen" container named docker-gen-nolabel, without the docker_gen label.
|
||||||
|
docker run --rm -d \
|
||||||
|
--name "$docker_gen" \
|
||||||
|
nginx:alpine > /dev/null
|
||||||
|
|
||||||
|
# This should target the nginx container whose id or name was obtained with
|
||||||
|
# the --volumes-from argument (nginx-volumes-from)
|
||||||
|
# and the docker-gen container whose id or name was obtained with
|
||||||
|
# the NGINX_DOCKER_GEN_CONTAINER environment variable (docker-gen-nolabel).
|
||||||
|
docker run --rm \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||||
|
--volumes-from "$nginx_vol" \
|
||||||
|
-e "NGINX_DOCKER_GEN_CONTAINER=$docker_gen" \
|
||||||
|
"$1" \
|
||||||
|
bash -c "$commands" 2>&1
|
||||||
|
|
||||||
|
# This should target the nginx container whose id or name was obtained with
|
||||||
|
# the NGINX_PROXY_CONTAINER environment variable (nginx-env-var)
|
||||||
|
# and the docker-gen container whose id or name was obtained with
|
||||||
|
# the NGINX_DOCKER_GEN_CONTAINER environment variable (docker-gen-nolabel)
|
||||||
|
docker run --rm \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||||
|
--volumes-from "$nginx_vol" \
|
||||||
|
-e "NGINX_PROXY_CONTAINER=$nginx_env" \
|
||||||
|
-e "NGINX_DOCKER_GEN_CONTAINER=$docker_gen" \
|
||||||
|
"$1" \
|
||||||
|
bash -c "$commands" 2>&1
|
||||||
|
|
||||||
|
# Spawn a nginx container named nginx-label, with the nginx_proxy label.
|
||||||
|
labeled_nginx1_cid="$(docker run --rm -d \
|
||||||
|
--name "$nginx_lbl" \
|
||||||
|
--label com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy \
|
||||||
|
nginx:alpine)"
|
||||||
|
|
||||||
|
# This should target the nginx container whose id or name was obtained with
|
||||||
|
# the nginx_proxy label (nginx-label)
|
||||||
|
# and the docker-gen container whose id or name was obtained with
|
||||||
|
# the NGINX_DOCKER_GEN_CONTAINER environment variable (docker-gen-nolabel)
|
||||||
|
docker run --rm \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||||
|
--volumes-from "$nginx_vol" \
|
||||||
|
-e "NGINX_PROXY_CONTAINER=$nginx_env" \
|
||||||
|
-e "NGINX_DOCKER_GEN_CONTAINER=$docker_gen" \
|
||||||
|
"$1" \
|
||||||
|
bash -c "$commands" 2>&1
|
||||||
|
|
||||||
|
docker stop "$nginx_lbl" > /dev/null
|
||||||
|
|
||||||
|
# Spawn a "fake docker-gen" container named docker-gen-label, with the docker_gen label.
|
||||||
|
labeled_docker_gen_cid="$(docker run --rm -d \
|
||||||
|
--name "$docker_gen_lbl" \
|
||||||
|
--label com.github.jrcs.letsencrypt_nginx_proxy_companion.docker_gen \
|
||||||
|
nginx:alpine)"
|
||||||
|
|
||||||
|
# This should target the nginx container whose id or name was obtained with
|
||||||
|
# the --volumes-from argument (nginx-volumes-from)
|
||||||
|
# and the docker-gen container whose id or name was obtained with
|
||||||
|
# the docker_gen label (docker-gen-label)
|
||||||
|
docker run --rm \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||||
|
--volumes-from "$nginx_vol" \
|
||||||
|
-e "NGINX_DOCKER_GEN_CONTAINER=$docker_gen" \
|
||||||
|
"$1" \
|
||||||
|
bash -c "$commands" 2>&1
|
||||||
|
|
||||||
|
# This should target the nginx container whose id or name was obtained with
|
||||||
|
# the NGINX_PROXY_CONTAINER environment variable (nginx-env-var)
|
||||||
|
# and the docker-gen container whose id or name was obtained with
|
||||||
|
# the docker_gen label (docker-gen-label)
|
||||||
|
docker run --rm \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||||
|
--volumes-from "$nginx_vol" \
|
||||||
|
-e "NGINX_PROXY_CONTAINER=$nginx_env" \
|
||||||
|
-e "NGINX_DOCKER_GEN_CONTAINER=$docker_gen" \
|
||||||
|
"$1" \
|
||||||
|
bash -c "$commands" 2>&1
|
||||||
|
|
||||||
|
# Spawn a nginx container named nginx-label, with the nginx_proxy label.
|
||||||
|
labeled_nginx2_cid="$(docker run --rm -d \
|
||||||
|
--name "$nginx_lbl" \
|
||||||
|
--label com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy \
|
||||||
|
nginx:alpine)"
|
||||||
|
|
||||||
|
# This should target the nginx container whose id or name was obtained with
|
||||||
|
# the nginx_proxy label (nginx-label)
|
||||||
|
# and the docker-gen container whose id or name was obtained with
|
||||||
|
# the docker_gen label (docker-gen-label)
|
||||||
|
docker run --rm \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||||
|
--volumes-from "$nginx_vol" \
|
||||||
|
-e "NGINX_PROXY_CONTAINER=$nginx_env" \
|
||||||
|
-e "NGINX_DOCKER_GEN_CONTAINER=$docker_gen" \
|
||||||
|
"$1" \
|
||||||
|
bash -c "$commands" 2>&1
|
||||||
|
|
||||||
|
cat > "${GITHUB_WORKSPACE}/test/tests/docker_api/expected-std-out.txt" <<EOF
|
||||||
|
Container $docker_gen received signal 1
|
||||||
|
Container $nginx_vol received signal 1
|
||||||
|
$docker_gen
|
||||||
|
$nginx_vol
|
||||||
|
Container $docker_gen received signal 1
|
||||||
|
Container $nginx_env received signal 1
|
||||||
|
$docker_gen
|
||||||
|
$nginx_env
|
||||||
|
Container $docker_gen received signal 1
|
||||||
|
Container $nginx_lbl received signal 1
|
||||||
|
$docker_gen
|
||||||
|
$labeled_nginx1_cid
|
||||||
|
Container $nginx_lbl received signal 3
|
||||||
|
Container $docker_gen_lbl received signal 1
|
||||||
|
Container $nginx_vol received signal 1
|
||||||
|
$labeled_docker_gen_cid
|
||||||
|
$nginx_vol
|
||||||
|
Container $docker_gen_lbl received signal 1
|
||||||
|
Container $nginx_env received signal 1
|
||||||
|
$labeled_docker_gen_cid
|
||||||
|
$nginx_env
|
||||||
|
Container $docker_gen_lbl received signal 1
|
||||||
|
Container $nginx_lbl received signal 1
|
||||||
|
$labeled_docker_gen_cid
|
||||||
|
$labeled_nginx2_cid
|
||||||
|
EOF
|
||||||
|
;;
|
||||||
|
|
||||||
|
esac
|
||||||
1
test/tests/force_renew/expected-std-out.txt
Normal file
1
test/tests/force_renew/expected-std-out.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
48
test/tests/force_renew/run.sh
Executable file
48
test/tests/force_renew/run.sh
Executable file
@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
## Test for the /app/force_renew script.
|
||||||
|
|
||||||
|
if [[ -z $GITHUB_ACTIONS ]]; then
|
||||||
|
le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")"
|
||||||
|
else
|
||||||
|
le_container_name="$(basename "${0%/*}")"
|
||||||
|
fi
|
||||||
|
run_le_container "${1:?}" "$le_container_name"
|
||||||
|
|
||||||
|
# Create the $domains array from comma separated domains in TEST_DOMAINS.
|
||||||
|
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"
|
||||||
|
|
||||||
|
# Cleanup function with EXIT trap
|
||||||
|
function cleanup {
|
||||||
|
# Remove the Nginx container silently.
|
||||||
|
docker rm --force "${domains[0]}" &> /dev/null
|
||||||
|
# Cleanup the files created by this run of the test to avoid foiling following test(s).
|
||||||
|
docker exec "$le_container_name" /app/cleanup_test_artifacts
|
||||||
|
# Stop the LE container
|
||||||
|
docker stop "$le_container_name" > /dev/null
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Run a nginx container for ${domains[0]}.
|
||||||
|
run_nginx_container --hosts "${domains[0]}"
|
||||||
|
|
||||||
|
# Wait for a symlink at /etc/nginx/certs/${domains[0]}.crt
|
||||||
|
# Grab the expiration time of the certificate
|
||||||
|
wait_for_symlink "${domains[0]}" "$le_container_name"
|
||||||
|
first_cert_expire="$(get_cert_expiration_epoch "${domains[0]}" "$le_container_name")"
|
||||||
|
|
||||||
|
# Just to be sure
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Issue a forced renewal
|
||||||
|
# Grab the expiration time of the renewed certificate
|
||||||
|
docker exec "$le_container_name" /app/force_renew &> /dev/null
|
||||||
|
second_cert_expire="$(get_cert_expiration_epoch "${domains[0]}" "$le_container_name")"
|
||||||
|
|
||||||
|
if ! [[ $second_cert_expire -gt $first_cert_expire ]]; then
|
||||||
|
echo "Certificate for ${domains[0]} was not correctly renewed."
|
||||||
|
echo "First certificate expiration epoch : $first_cert_expire."
|
||||||
|
echo "Second certificate expiration epoch : $second_cert_expire."
|
||||||
|
elif [[ "${DRY_RUN:-}" == 1 ]]; then
|
||||||
|
echo "Certificate for ${domains[0]} was correctly renewed."
|
||||||
|
fi
|
||||||
7
test/tests/location_config/expected-std-out.txt
Normal file
7
test/tests/location_config/expected-std-out.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
*.bar.baz.example.com
|
||||||
|
*.baz.example.com
|
||||||
|
*.example.com
|
||||||
|
foo.bar.baz.example.*
|
||||||
|
foo.bar.baz.*
|
||||||
|
foo.bar.*
|
||||||
|
foo.*
|
||||||
171
test/tests/location_config/run.sh
Executable file
171
test/tests/location_config/run.sh
Executable file
@ -0,0 +1,171 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
## Test for automatic location configuration.
|
||||||
|
|
||||||
|
# Set variables
|
||||||
|
test_comment='### This is a test comment'
|
||||||
|
vhost_path='/etc/nginx/vhost.d'
|
||||||
|
|
||||||
|
# Create custom location configuration file to be bind mounted
|
||||||
|
location_file="${GITHUB_WORKSPACE}/test/tests/location_config/le2.wtf"
|
||||||
|
echo "$test_comment" > "$location_file"
|
||||||
|
|
||||||
|
# Create le1.wtf configuration file, *.le3.wtf and test.* from inside the nginx container
|
||||||
|
docker exec "$NGINX_CONTAINER_NAME" sh -c "echo '### This is a test comment' > /etc/nginx/vhost.d/le1.wtf"
|
||||||
|
docker exec "$NGINX_CONTAINER_NAME" sh -c "echo '### This is a test comment' > /etc/nginx/vhost.d/\*.example.com"
|
||||||
|
docker exec "$NGINX_CONTAINER_NAME" sh -c "echo '### This is a test comment' > /etc/nginx/vhost.d/test.\*"
|
||||||
|
|
||||||
|
# Zero the default configuration file.
|
||||||
|
docker exec "$NGINX_CONTAINER_NAME" sh -c "echo '' > /etc/nginx/vhost.d/default"
|
||||||
|
|
||||||
|
if [[ -z $GITHUB_ACTIONS ]]; then
|
||||||
|
le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")"
|
||||||
|
else
|
||||||
|
le_container_name="$(basename "${0%/*}")"
|
||||||
|
fi
|
||||||
|
run_le_container "${1:?}" "$le_container_name" "--volume $location_file:$vhost_path/le2.wtf"
|
||||||
|
|
||||||
|
# Create the $domains array from comma separated domains in TEST_DOMAINS.
|
||||||
|
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"
|
||||||
|
|
||||||
|
# Cleanup function with EXIT trap
|
||||||
|
function cleanup {
|
||||||
|
# Cleanup the files created by this run of the test to avoid foiling following test(s).
|
||||||
|
docker exec "$le_container_name" /app/cleanup_test_artifacts --location-config
|
||||||
|
# Stop the LE container
|
||||||
|
docker stop "$le_container_name" > /dev/null
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Check if the ACME location configuration was correctly applied (ie only once) to the target file
|
||||||
|
function check_location {
|
||||||
|
local container="${1:?}"
|
||||||
|
local path="${2:?}"
|
||||||
|
local start_comment='## Start of configuration add by letsencrypt container'
|
||||||
|
local end_comment='## End of configuration add by letsencrypt container'
|
||||||
|
|
||||||
|
if [[ "$(docker exec "$container" grep -c "$start_comment" "$path")" != 1 ]]; then
|
||||||
|
return 1
|
||||||
|
elif [[ "$(docker exec "$container" grep -c "$end_comment" "$path")" != 1 ]]; then
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# check the wildcard location enumeration function
|
||||||
|
docker exec "$le_container_name" bash -c 'source /app/functions.sh; enumerate_wildcard_locations foo.bar.baz.example.com'
|
||||||
|
|
||||||
|
# default configuration file should be empty
|
||||||
|
config_path="$vhost_path/default"
|
||||||
|
if docker exec "$le_container_name" [ ! -s "$config_path" ]; then
|
||||||
|
echo "$config_path should be empty at container startup:"
|
||||||
|
docker exec "$le_container_name" cat "$config_path"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# custom configuration files should only contains the test comment
|
||||||
|
for domain in "${domains[@]:0:2}" '*.example.com' 'test.*'; do
|
||||||
|
config_path="$vhost_path/$domain"
|
||||||
|
if check_location "$le_container_name" "$config_path"; then
|
||||||
|
echo "Unexpected location configuration on $config_path at container startup:"
|
||||||
|
docker exec "$le_container_name" cat "$config_path"
|
||||||
|
elif ! docker exec "$le_container_name" grep -q "$test_comment" "$config_path"; then
|
||||||
|
echo "$config_path should have test comment at container startup:"
|
||||||
|
docker exec "$le_container_name" cat "$config_path"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# le3.wtf configuration file should not exist
|
||||||
|
config_path="$vhost_path/${domains[2]}"
|
||||||
|
if docker exec "$le_container_name" [ -e "$config_path" ]; then
|
||||||
|
echo "$config_path should not exist at container startup :"
|
||||||
|
docker exec "$le_container_name" ls -lh "$config_path"
|
||||||
|
docker exec "$le_container_name" cat "$config_path"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add default location configuration then check
|
||||||
|
config_path="$vhost_path/default"
|
||||||
|
docker exec "$le_container_name" bash -c 'source /app/functions.sh; add_location_configuration'
|
||||||
|
if ! check_location "$le_container_name" "$config_path" ; then
|
||||||
|
echo "Unexpected location configuration on $config_path after call to add_location_configuration:"
|
||||||
|
docker exec "$le_container_name" cat "$config_path"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add le1.wtf and le2.wtf location configurations then check
|
||||||
|
for domain in "${domains[@]:0:2}"; do
|
||||||
|
config_path="$vhost_path/$domain"
|
||||||
|
docker exec "$le_container_name" bash -c "source /app/functions.sh; add_location_configuration $domain"
|
||||||
|
if ! check_location "$le_container_name" "$config_path" ; then
|
||||||
|
echo "Unexpected location configuration on $config_path after call to add_location_configuration $domain:"
|
||||||
|
docker exec "$le_container_name" cat "$config_path"
|
||||||
|
elif ! docker exec "$le_container_name" grep -q "$test_comment" "$config_path"; then
|
||||||
|
echo "$config_path should still have test comment after call to add_location_configuration $domain:"
|
||||||
|
docker exec "$le_container_name" cat "$config_path"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Adding subdomain.example.com location configurations should use the *.example.com file
|
||||||
|
domain="subdomain.example.com"
|
||||||
|
config_path="$vhost_path/*.example.com"
|
||||||
|
docker exec "$le_container_name" bash -c "source /app/functions.sh; add_location_configuration $domain"
|
||||||
|
if ! check_location "$le_container_name" "$config_path" ; then
|
||||||
|
echo "Unexpected location configuration on $config_path after call to add_location_configuration $domain:"
|
||||||
|
docker exec "$le_container_name" cat "$config_path"
|
||||||
|
elif ! docker exec "$le_container_name" grep -q "$test_comment" "$config_path"; then
|
||||||
|
echo "$config_path should still have test comment after call to add_location_configuration $domain:"
|
||||||
|
docker exec "$le_container_name" cat "$config_path"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Adding test.domain.tld location configurations should use the test.* file
|
||||||
|
domain="test.domain.tld"
|
||||||
|
config_path="$vhost_path/test.*"
|
||||||
|
docker exec "$le_container_name" bash -c "source /app/functions.sh; add_location_configuration $domain"
|
||||||
|
if ! check_location "$le_container_name" "$config_path" ; then
|
||||||
|
echo "Unexpected location configuration on $config_path after call to add_location_configuration $domain:"
|
||||||
|
docker exec "$le_container_name" cat "$config_path"
|
||||||
|
elif ! docker exec "$le_container_name" grep -q "$test_comment" "$config_path"; then
|
||||||
|
echo "$config_path should still have test comment after call to add_location_configuration $domain:"
|
||||||
|
docker exec "$le_container_name" cat "$config_path"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove all location configurations
|
||||||
|
docker exec "$le_container_name" bash -c "source /app/functions.sh; remove_all_location_configurations"
|
||||||
|
|
||||||
|
# default configuration file should be empty again
|
||||||
|
config_path="$vhost_path/default"
|
||||||
|
if docker exec "$le_container_name" [ ! -s "$config_path" ]; then
|
||||||
|
echo "$config_path should be empty after call to remove_all_location_configurations:"
|
||||||
|
docker exec "$le_container_name" cat "$config_path"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Custom configuration files should have reverted to only containing the test comment
|
||||||
|
for domain in "${domains[@]:0:2}" '*.example.com' 'test.*'; do
|
||||||
|
config_path="$vhost_path/$domain"
|
||||||
|
if check_location "$le_container_name" "$config_path"; then
|
||||||
|
echo "Unexpected location configuration on $config_path after call to remove_all_location_configurations:"
|
||||||
|
docker exec "$le_container_name" cat "$config_path"
|
||||||
|
elif ! docker exec "$le_container_name" grep -q "$test_comment" "$config_path"; then
|
||||||
|
echo "$config_path should still have test comment after call to remove_all_location_configurations:"
|
||||||
|
docker exec "$le_container_name" cat "$config_path"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Should not be used by anything, but potentially matches an enumerate_wildcard_locations file glob.
|
||||||
|
docker exec "$NGINX_CONTAINER_NAME" touch /etc/nginx/vhost.d/le3.pizza
|
||||||
|
docker exec "$le_container_name" touch le3.pizza
|
||||||
|
|
||||||
|
# Trying to add location configuration to non existing le3.wtf should only configure default
|
||||||
|
docker exec "$le_container_name" bash -c "source /app/functions.sh; add_location_configuration ${domains[2]}"
|
||||||
|
|
||||||
|
config_path="$vhost_path/${domains[2]}"
|
||||||
|
if docker exec "$le_container_name" [ -e "$config_path" ]; then
|
||||||
|
echo "$config_path should not exist after call to add_location_configuration ${domains[2]}:"
|
||||||
|
docker exec "$le_container_name" ls -lh "$config_path"
|
||||||
|
docker exec "$le_container_name" cat "$config_path"
|
||||||
|
fi
|
||||||
|
|
||||||
|
config_path="$vhost_path/default"
|
||||||
|
if ! check_location "$le_container_name" "$config_path" ; then
|
||||||
|
echo "Unexpected location configuration on $config_path after call to add_location_configuration ${domains[2]}:"
|
||||||
|
docker exec "$le_container_name" cat "$config_path"
|
||||||
|
fi
|
||||||
1
test/tests/ocsp_must_staple/expected-std-out.txt
Normal file
1
test/tests/ocsp_must_staple/expected-std-out.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
52
test/tests/ocsp_must_staple/run.sh
Executable file
52
test/tests/ocsp_must_staple/run.sh
Executable file
@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
## Test for OCSP Must-Staple extension.
|
||||||
|
|
||||||
|
if [[ -z $GITHUB_ACTIONS ]]; then
|
||||||
|
le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")"
|
||||||
|
else
|
||||||
|
le_container_name="$(basename "${0%/*}")"
|
||||||
|
fi
|
||||||
|
run_le_container "${1:?}" "$le_container_name"
|
||||||
|
|
||||||
|
# Create the $domains array from comma separated domains in TEST_DOMAINS.
|
||||||
|
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"
|
||||||
|
|
||||||
|
# Cleanup function with EXIT trap
|
||||||
|
function cleanup {
|
||||||
|
# Remove any remaining Nginx container(s) silently.
|
||||||
|
for domain in "${domains[0]}" "${domains[1]}"; do
|
||||||
|
docker rm --force "$domain" &> /dev/null
|
||||||
|
done
|
||||||
|
# Cleanup the files created by this run of the test to avoid foiling following test(s).
|
||||||
|
docker exec "$le_container_name" /app/cleanup_test_artifacts
|
||||||
|
# Stop the LE container
|
||||||
|
docker stop "$le_container_name" > /dev/null
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Run an nginx container with ACME_OCSP=true
|
||||||
|
run_nginx_container --hosts "${domains[0]}" --cli-args "--env ACME_OCSP=true"
|
||||||
|
|
||||||
|
# Run an second nginx container without ACME_OCSP=true
|
||||||
|
run_nginx_container --hosts "${domains[1]}"
|
||||||
|
|
||||||
|
# Wait for the symlink to the ${domains[0]} certificate
|
||||||
|
wait_for_symlink "${domains[0]}" "$le_container_name"
|
||||||
|
|
||||||
|
# Check if the OCSP Must-Staple extension is present in the ${domains[0]} certificate
|
||||||
|
if docker exec "$le_container_name" openssl x509 -in "/etc/nginx/certs/${domains[0]}/cert.pem" -text -noout | grep -q -E '1\.3\.6\.1\.5\.5\.7\.1\.24|status_request'; then
|
||||||
|
[[ "${DRY_RUN:-}" == 1 ]] && echo "The OCSP Must-Staple extension is present on the ${domains[0]} certificate."
|
||||||
|
else
|
||||||
|
echo "The OCSP Must-Staple extension is absent from the ${domains[0]} certificate."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wait for the symlink to the ${domains[1]} certificate
|
||||||
|
wait_for_symlink "${domains[1]}" "$le_container_name"
|
||||||
|
|
||||||
|
# Check if the OCSP Must-Staple extension is absent from the ${domains[1]} certificate
|
||||||
|
if docker exec "$le_container_name" openssl x509 -in "/etc/nginx/certs/${domains[1]}/cert.pem" -text -noout | grep -q -E '1\.3\.6\.1\.5\.5\.7\.1\.24|status_request'; then
|
||||||
|
echo "The OCSP Must-Staple extension is present on the ${domains[1]} certificate."
|
||||||
|
elif [[ "${DRY_RUN:-}" == 1 ]]; then
|
||||||
|
echo "The OCSP Must-Staple extension is absent from the ${domains[1]} certificate."
|
||||||
|
fi
|
||||||
1
test/tests/permissions_custom/expected-std-out.txt
Normal file
1
test/tests/permissions_custom/expected-std-out.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
97
test/tests/permissions_custom/run.sh
Executable file
97
test/tests/permissions_custom/run.sh
Executable file
@ -0,0 +1,97 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
## Test for sensitive files and folders permissions
|
||||||
|
|
||||||
|
files_uid=1000
|
||||||
|
files_gid=1001
|
||||||
|
files_perms=640
|
||||||
|
folders_perms=750
|
||||||
|
|
||||||
|
if [[ -z $GITHUB_ACTIONS ]]; then
|
||||||
|
le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")"
|
||||||
|
else
|
||||||
|
le_container_name="$(basename "${0%/*}")"
|
||||||
|
fi
|
||||||
|
run_le_container "${1:?}" "$le_container_name" \
|
||||||
|
"--env FILES_UID=$files_uid --env FILES_GID=$files_gid --env FILES_PERMS=$files_perms --env FOLDERS_PERMS=$folders_perms"
|
||||||
|
|
||||||
|
# Create the $domains array from comma separated domains in TEST_DOMAINS.
|
||||||
|
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"
|
||||||
|
|
||||||
|
# Cleanup function with EXIT trap
|
||||||
|
function cleanup {
|
||||||
|
# Remove the ${domains[0]} Nginx container silently.
|
||||||
|
docker rm --force "${domains[0]}" &> /dev/null
|
||||||
|
# Cleanup the files created by this run of the test to avoid foiling following test(s).
|
||||||
|
docker exec "$le_container_name" /app/cleanup_test_artifacts
|
||||||
|
# Stop the LE container
|
||||||
|
docker stop "$le_container_name" > /dev/null
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Run an nginx container for ${domains[0]}.
|
||||||
|
run_nginx_container --hosts "${domains[0]}"
|
||||||
|
|
||||||
|
# Wait for the cert symlink.
|
||||||
|
wait_for_symlink "${domains[0]}" "$le_container_name"
|
||||||
|
|
||||||
|
# Array of folder paths to test
|
||||||
|
folders=( \
|
||||||
|
[0]="/etc/nginx/certs/${domains[0]}" \
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test folder paths
|
||||||
|
for folder in "${folders[@]}"; do
|
||||||
|
ownership_and_permissions="$(docker exec "$le_container_name" stat -c %u:%g:%a "$folder")"
|
||||||
|
if [[ "$ownership_and_permissions" != ${files_uid}:${files_gid}:${folders_perms} ]]; then
|
||||||
|
echo "Expected ${files_uid}:${files_gid}:${folders_perms} on ${folder}, found ${ownership_and_permissions}."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Array of symlinks paths to test
|
||||||
|
symlinks=( \
|
||||||
|
[0]="/etc/nginx/certs/${domains[0]}.crt" \
|
||||||
|
[1]="/etc/nginx/certs/${domains[0]}.key" \
|
||||||
|
[2]="/etc/nginx/certs/${domains[0]}.chain.pem" \
|
||||||
|
[3]="/etc/nginx/certs/${domains[0]}.dhparam.pem" \
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test symlinks paths
|
||||||
|
for symlink in "${symlinks[@]}"; do
|
||||||
|
ownership="$(docker exec "$le_container_name" stat -c %u:%g "$symlink")"
|
||||||
|
if [[ "$ownership" != ${files_uid}:${files_gid} ]]; then
|
||||||
|
echo "Expected ${files_uid}:${files_gid} on ${symlink}, found ${ownership}."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Array of private file paths to test
|
||||||
|
private_files=( \
|
||||||
|
[0]="/etc/nginx/certs/default.key" \
|
||||||
|
[1]="/etc/nginx/certs/${domains[0]}/key.pem" \
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test private file paths
|
||||||
|
for file in "${private_files[@]}"; do
|
||||||
|
ownership_and_permissions="$(docker exec "$le_container_name" stat -c %u:%g:%a "$file")"
|
||||||
|
if [[ "$ownership_and_permissions" != ${files_uid}:${files_gid}:${files_perms} ]]; then
|
||||||
|
echo "Expected ${files_uid}:${files_gid}:${files_perms} on ${file}, found ${ownership_and_permissions}."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Array of public files paths to test
|
||||||
|
public_files=( \
|
||||||
|
[0]="/etc/nginx/certs/${domains[0]}/.companion" \
|
||||||
|
[1]="/etc/nginx/certs/${domains[0]}/cert.pem" \
|
||||||
|
[2]="/etc/nginx/certs/${domains[0]}/chain.pem" \
|
||||||
|
[3]="/etc/nginx/certs/${domains[0]}/fullchain.pem" \
|
||||||
|
[4]="/etc/nginx/certs/default.crt" \
|
||||||
|
[5]="/etc/nginx/certs/dhparam.pem" \
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test public file paths
|
||||||
|
for file in "${public_files[@]}"; do
|
||||||
|
ownership_and_permissions="$(docker exec "$le_container_name" stat -c %u:%g:%a "$file")"
|
||||||
|
if [[ "$ownership_and_permissions" != ${files_uid}:${files_gid}:644 ]]; then
|
||||||
|
echo "Expected ${files_uid}:${files_gid}:644 on ${file}, found ${ownership_and_permissions}."
|
||||||
|
fi
|
||||||
|
done
|
||||||
1
test/tests/permissions_default/expected-std-out.txt
Normal file
1
test/tests/permissions_default/expected-std-out.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
91
test/tests/permissions_default/run.sh
Executable file
91
test/tests/permissions_default/run.sh
Executable file
@ -0,0 +1,91 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
## Test for sensitive files and folders permissions
|
||||||
|
|
||||||
|
if [[ -z $GITHUB_ACTIONS ]]; then
|
||||||
|
le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")"
|
||||||
|
else
|
||||||
|
le_container_name="$(basename "${0%/*}")"
|
||||||
|
fi
|
||||||
|
run_le_container "${1:?}" "$le_container_name"
|
||||||
|
|
||||||
|
# Create the $domains array from comma separated domains in TEST_DOMAINS.
|
||||||
|
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"
|
||||||
|
|
||||||
|
# Cleanup function with EXIT trap
|
||||||
|
function cleanup {
|
||||||
|
# Remove the ${domains[0]} Nginx container silently.
|
||||||
|
docker rm --force "${domains[0]}" &> /dev/null
|
||||||
|
# Cleanup the files created by this run of the test to avoid foiling following test(s).
|
||||||
|
docker exec "$le_container_name" /app/cleanup_test_artifacts
|
||||||
|
# Stop the LE container
|
||||||
|
docker stop "$le_container_name" > /dev/null
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Run an nginx container for ${domains[0]}.
|
||||||
|
run_nginx_container --hosts "${domains[0]}"
|
||||||
|
|
||||||
|
# Wait for the cert symlink.
|
||||||
|
wait_for_symlink "${domains[0]}" "$le_container_name"
|
||||||
|
|
||||||
|
# Array of folder paths to test
|
||||||
|
folders=( \
|
||||||
|
[0]="/etc/nginx/certs/${domains[0]}" \
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test folder paths
|
||||||
|
for folder in "${folders[@]}"; do
|
||||||
|
ownership_and_permissions="$(docker exec "$le_container_name" stat -c %u:%g:%a "$folder")"
|
||||||
|
if [[ "$ownership_and_permissions" != 0:0:755 ]]; then
|
||||||
|
echo "Expected 0:0:755 on ${folder}, found ${ownership_and_permissions}."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Array of symlinks paths to test
|
||||||
|
symlinks=( \
|
||||||
|
[0]="/etc/nginx/certs/${domains[0]}.crt" \
|
||||||
|
[1]="/etc/nginx/certs/${domains[0]}.key" \
|
||||||
|
[2]="/etc/nginx/certs/${domains[0]}.chain.pem" \
|
||||||
|
[3]="/etc/nginx/certs/${domains[0]}.dhparam.pem" \
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test symlinks paths
|
||||||
|
for symlink in "${symlinks[@]}"; do
|
||||||
|
ownership="$(docker exec "$le_container_name" stat -c %u:%g "$symlink")"
|
||||||
|
if [[ "$ownership" != 0:0 ]]; then
|
||||||
|
echo "Expected 0:0 on ${symlink}, found ${ownership}."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Array of private file paths to test
|
||||||
|
private_files=( \
|
||||||
|
[0]="/etc/nginx/certs/default.key" \
|
||||||
|
[1]="/etc/nginx/certs/${domains[0]}/key.pem" \
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test private file paths
|
||||||
|
for file in "${private_files[@]}"; do
|
||||||
|
ownership_and_permissions="$(docker exec "$le_container_name" stat -c %u:%g:%a "$file")"
|
||||||
|
if [[ "$ownership_and_permissions" != 0:0:644 ]]; then
|
||||||
|
echo "Expected 0:0:644 on ${file}, found ${ownership_and_permissions}."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Array of public files paths to test
|
||||||
|
public_files=( \
|
||||||
|
[0]="/etc/nginx/certs/${domains[0]}/.companion" \
|
||||||
|
[1]="/etc/nginx/certs/${domains[0]}/cert.pem" \
|
||||||
|
[2]="/etc/nginx/certs/${domains[0]}/chain.pem" \
|
||||||
|
[3]="/etc/nginx/certs/${domains[0]}/fullchain.pem" \
|
||||||
|
[4]="/etc/nginx/certs/default.crt" \
|
||||||
|
[5]="/etc/nginx/certs/dhparam.pem" \
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test public file paths
|
||||||
|
for file in "${public_files[@]}"; do
|
||||||
|
ownership_and_permissions="$(docker exec "$le_container_name" stat -c %u:%g:%a "$file")"
|
||||||
|
if [[ "$ownership_and_permissions" != 0:0:644 ]]; then
|
||||||
|
echo "Expected 0:0:644 on ${file}, found ${ownership_and_permissions}."
|
||||||
|
fi
|
||||||
|
done
|
||||||
1
test/tests/private_keys/expected-std-out.txt
Normal file
1
test/tests/private_keys/expected-std-out.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
56
test/tests/private_keys/run.sh
Executable file
56
test/tests/private_keys/run.sh
Executable file
@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
## Test for private keys types
|
||||||
|
|
||||||
|
if [[ -z $GITHUB_ACTIONS ]]; then
|
||||||
|
le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")"
|
||||||
|
else
|
||||||
|
le_container_name="$(basename "${0%/*}")"
|
||||||
|
fi
|
||||||
|
run_le_container "${1:?}" "$le_container_name"
|
||||||
|
|
||||||
|
# Create the $domains array from comma separated domains in TEST_DOMAINS.
|
||||||
|
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"
|
||||||
|
|
||||||
|
# Cleanup function with EXIT trap
|
||||||
|
function cleanup {
|
||||||
|
# Remove any remaining Nginx container(s) silently.
|
||||||
|
for key in "${!key_types[@]}"; do
|
||||||
|
docker rm --force "${key}" &> /dev/null
|
||||||
|
done
|
||||||
|
# Cleanup the files created by this run of the test to avoid foiling following test(s).
|
||||||
|
docker exec "$le_container_name" /app/cleanup_test_artifacts
|
||||||
|
# Stop the LE container
|
||||||
|
docker stop "$le_container_name" > /dev/null
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
declare -A key_types
|
||||||
|
key_types=( \
|
||||||
|
['2048']='RSA Public-Key: (2048 bit)' \
|
||||||
|
['3072']='RSA Public-Key: (3072 bit)' \
|
||||||
|
['4096']='RSA Public-Key: (4096 bit)' \
|
||||||
|
['ec-256']='prime256v1' \
|
||||||
|
['ec-384']='secp384r1' \
|
||||||
|
)
|
||||||
|
|
||||||
|
for key in "${!key_types[@]}"; do
|
||||||
|
|
||||||
|
# Run an Nginx container with the wanted key type.
|
||||||
|
run_nginx_container --hosts "${domains[0]}" --name "${key}" --cli-args "--env LETSENCRYPT_KEYSIZE=${key}"
|
||||||
|
|
||||||
|
# Grep the expected string from the public key in text form.
|
||||||
|
if wait_for_symlink "${domains[0]}" "$le_container_name"; then
|
||||||
|
public_key=$(docker exec "$le_container_name" openssl pkey -in "/etc/nginx/certs/${domains[0]}.key" -noout -text_pub)
|
||||||
|
if ! grep -q "${key_types[$key]}" <<< "$public_key"; then
|
||||||
|
echo "Keys for test $key were not of the correct type, expected ${key_types[$key]} and got the following:"
|
||||||
|
echo "$public_key"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "${key_types[$key]} key test timed out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker stop "${key}" &> /dev/null
|
||||||
|
docker exec "$le_container_name" /app/cleanup_test_artifacts
|
||||||
|
|
||||||
|
done
|
||||||
1
test/tests/symlinks/expected-std-out.txt
Normal file
1
test/tests/symlinks/expected-std-out.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Symlink for lim.it certificate was not generated under one minute, timing out.
|
||||||
110
test/tests/symlinks/run.sh
Executable file
110
test/tests/symlinks/run.sh
Executable file
@ -0,0 +1,110 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
## Test for symlink creation / removal.
|
||||||
|
|
||||||
|
if [[ -z $GITHUB_ACTIONS ]]; then
|
||||||
|
le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")"
|
||||||
|
else
|
||||||
|
le_container_name="$(basename "${0%/*}")"
|
||||||
|
fi
|
||||||
|
run_le_container "${1:?}" "$le_container_name"
|
||||||
|
|
||||||
|
# Create the $domains array from comma separated domains in TEST_DOMAINS.
|
||||||
|
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"
|
||||||
|
|
||||||
|
# Cleanup function with EXIT trap
|
||||||
|
function cleanup {
|
||||||
|
# Remove all remaining nginx containers silently
|
||||||
|
docker rm --force \
|
||||||
|
symlink-le1-le2 \
|
||||||
|
symlink-le1-le2-le3 \
|
||||||
|
symlink-le2 \
|
||||||
|
symlink-le3 \
|
||||||
|
symlink-lim-le2 \
|
||||||
|
&> /dev/null
|
||||||
|
# Cleanup the files created by this run of the test to avoid foiling following test(s).
|
||||||
|
docker exec "$le_container_name" /app/cleanup_test_artifacts
|
||||||
|
# Stop the LE container
|
||||||
|
docker stop "$le_container_name" > /dev/null
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Run a nginx container for the firs two domain in the $domains array ...
|
||||||
|
run_nginx_container --hosts "${domains[0]},${domains[1]}" --name "symlink-le1-le2"
|
||||||
|
|
||||||
|
# ... plus another nginx container for the third domain.
|
||||||
|
run_nginx_container --hosts "${domains[2]}" --name "symlink-le3"
|
||||||
|
|
||||||
|
# Wait for a file at /etc/nginx/certs/$domain/cert.pem
|
||||||
|
wait_for_symlink "${domains[0]}" "$le_container_name" "./${domains[0]}/fullchain.pem"
|
||||||
|
wait_for_symlink "${domains[1]}" "$le_container_name" "./${domains[0]}/fullchain.pem"
|
||||||
|
wait_for_symlink "${domains[2]}" "$le_container_name" "./${domains[2]}/fullchain.pem"
|
||||||
|
|
||||||
|
# Create a fake le4.wtf custom certificate and key
|
||||||
|
docker exec "$le_container_name" mkdir -p /etc/nginx/certs/le4.wtf
|
||||||
|
docker exec "$le_container_name" cp /etc/nginx/certs/le1.wtf/fullchain.pem /etc/nginx/certs/le4.wtf/
|
||||||
|
docker exec "$le_container_name" cp /etc/nginx/certs/le1.wtf/key.pem /etc/nginx/certs/le4.wtf/
|
||||||
|
docker exec "$le_container_name" bash -c 'cd /etc/nginx/certs; ln -s ./le4.wtf/fullchain.pem ./le4.wtf.crt'
|
||||||
|
docker exec "$le_container_name" bash -c 'cd /etc/nginx/certs; ln -s ./le4.wtf/key.pem ./le4.wtf.key'
|
||||||
|
|
||||||
|
# Stop the nginx containers for ${domains[0]} and ${domains[1]} silently,
|
||||||
|
# then check if the corresponding symlinks are removed.
|
||||||
|
docker stop "symlink-le1-le2" > /dev/null
|
||||||
|
for domain in "${domains[@]::2}"; do
|
||||||
|
wait_for_symlink_rm "$domain" "$le_container_name"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check if ${domains[2]} symlink is still there
|
||||||
|
docker exec "$le_container_name" [ -L "/etc/nginx/certs/${domains[2]}.crt" ] \
|
||||||
|
|| echo "Symlink to ${domains[2]} certificate was removed."
|
||||||
|
|
||||||
|
# Stop the nginx containers for ${domains[2]} silently,
|
||||||
|
# then check if the corresponding symlink is removed.
|
||||||
|
docker stop "symlink-le3" > /dev/null
|
||||||
|
wait_for_symlink_rm "${domains[2]}" "$le_container_name"
|
||||||
|
|
||||||
|
# Start the nginx containers for ${domains[2]} again,
|
||||||
|
# and check if the corresponding symlink is re-created.
|
||||||
|
run_nginx_container --hosts "${domains[2]}" --name "symlink-le3"
|
||||||
|
wait_for_symlink "${domains[2]}" "$le_container_name" "./${domains[2]}/fullchain.pem"
|
||||||
|
|
||||||
|
# Stop the nginx containers for ${domains[2]} silently and wait for symlink removal.
|
||||||
|
docker stop "symlink-le3" > /dev/null
|
||||||
|
wait_for_symlink_rm "${domains[2]}" "$le_container_name"
|
||||||
|
|
||||||
|
# Move ${domains[2]} to a san certificate with ${domains[0]} and ${domains[1]}
|
||||||
|
run_nginx_container --hosts "${domains[0]},${domains[1]},${domains[2]}" --name "symlink-le1-le2-le3"
|
||||||
|
|
||||||
|
# Check where the symlink points (should be ./le1.wtf/fullchain.pem)
|
||||||
|
wait_for_symlink "${domains[2]}" "$le_container_name" "./${domains[0]}/fullchain.pem"
|
||||||
|
|
||||||
|
# Stop the nginx container silently.
|
||||||
|
docker stop "symlink-le1-le2-le3" > /dev/null
|
||||||
|
|
||||||
|
# Check if the symlinks are correctly removed
|
||||||
|
for domain in "${domains[@]}"; do
|
||||||
|
wait_for_symlink_rm "$domain" "$le_container_name"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Move ${domains[1]} to a new single domain certificate
|
||||||
|
run_nginx_container --hosts "${domains[1]}" --name "symlink-le2"
|
||||||
|
|
||||||
|
# Check where the symlink points (should be ./le2.wtf/fullchain.pem)
|
||||||
|
wait_for_symlink "${domains[1]}" "$le_container_name" "./${domains[1]}/fullchain.pem"
|
||||||
|
|
||||||
|
# Stop the nginx container silently and try to put ${domains[1]} on a
|
||||||
|
# san certificate whose authorization will fail.
|
||||||
|
docker stop "symlink-le2" > /dev/null
|
||||||
|
run_nginx_container --hosts "lim.it,${domains[1]}" --name "symlink-lim-le2"
|
||||||
|
|
||||||
|
# The symlink creation for lim.it should time out, and the ${domains[1]}
|
||||||
|
# symlink should still point to ./le2.wtf/fullchain.pem
|
||||||
|
wait_for_symlink "lim.it" "$le_container_name"
|
||||||
|
wait_for_symlink "${domains[1]}" "$le_container_name" "./${domains[1]}/fullchain.pem"
|
||||||
|
|
||||||
|
# Aaaaaand stop the container.
|
||||||
|
docker stop "symlink-lim-le2" > /dev/null
|
||||||
|
|
||||||
|
# Check if the custom certificate is still there
|
||||||
|
docker exec "$le_container_name" [ -f /etc/nginx/certs/le4.wtf.crt ] \
|
||||||
|
|| echo "Custom certificate for le4.wtf was removed."
|
||||||
315
test/tests/test-functions.sh
Executable file
315
test/tests/test-functions.sh
Executable file
@ -0,0 +1,315 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Get the first domain of a comma separated list.
|
||||||
|
function get_base_domain {
|
||||||
|
awk -F ',' '{print $1}' <<< "${1:?}" | tr -d ' ' | sed 's/\.$//'
|
||||||
|
}
|
||||||
|
export -f get_base_domain
|
||||||
|
|
||||||
|
|
||||||
|
# Run a acme-companion container
|
||||||
|
function run_le_container {
|
||||||
|
local image="${1:?}"
|
||||||
|
local name="${2:?}"
|
||||||
|
local cli_args_str="${3:-}"
|
||||||
|
local -a cli_args_arr
|
||||||
|
for arg in $cli_args_str; do
|
||||||
|
cli_args_arr+=("$arg")
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$SETUP" == '3containers' ]]; then
|
||||||
|
cli_args_arr+=(--env "NGINX_DOCKER_GEN_CONTAINER=$DOCKER_GEN_CONTAINER_NAME")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$ACME_CA" == 'boulder' ]]; then
|
||||||
|
cli_args_arr+=(--env "ACME_CA_URI=http://boulder:4001/directory")
|
||||||
|
cli_args_arr+=(--network boulder_bluenet)
|
||||||
|
elif [[ "$ACME_CA" == 'pebble' ]]; then
|
||||||
|
cli_args_arr+=(--env "ACME_CA_URI=https://pebble:14000/dir")
|
||||||
|
cli_args_arr+=(--env "CA_BUNDLE=/pebble.minica.pem")
|
||||||
|
cli_args_arr+=(--network acme_net)
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if docker run -d \
|
||||||
|
--name "$name" \
|
||||||
|
--volumes-from "$NGINX_CONTAINER_NAME" \
|
||||||
|
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||||
|
--volume "${GITHUB_WORKSPACE}/pebble.minica.pem:/pebble.minica.pem" \
|
||||||
|
"${cli_args_arr[@]}" \
|
||||||
|
--env "DOCKER_GEN_WAIT=500ms:2s" \
|
||||||
|
--env "TEST_MODE=true" \
|
||||||
|
--env "DHPARAM_BITS=256" \
|
||||||
|
--env "DEBUG=1" \
|
||||||
|
--label com.github.jrcs.letsencrypt_nginx_proxy_companion.test_suite \
|
||||||
|
"$image" > /dev/null; \
|
||||||
|
then
|
||||||
|
[[ "${DRY_RUN:-}" == 1 ]] && echo "Started letsencrypt container for test ${name%%_2*}"
|
||||||
|
else
|
||||||
|
echo "Could not start letsencrypt container for test ${name%%_2*}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
export -f run_le_container
|
||||||
|
|
||||||
|
# Run an nginx container
|
||||||
|
function run_nginx_container {
|
||||||
|
local -a cli_args_arr
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
local flag="$1"
|
||||||
|
|
||||||
|
case $flag in
|
||||||
|
-h|--hosts)
|
||||||
|
local le_host="${2:?}"
|
||||||
|
local virtual_host="${le_host// /}"; virtual_host="${virtual_host//.,/,}"; virtual_host="${virtual_host%,}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
|
||||||
|
-n|--name)
|
||||||
|
local container_name="${2:?}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
|
||||||
|
-c|--cli-args)
|
||||||
|
local cli_args_str="${2:?}"
|
||||||
|
for arg in $cli_args_str; do
|
||||||
|
cli_args_arr+=("$arg")
|
||||||
|
done
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
|
||||||
|
*) #Unknown option
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$ACME_CA" == 'boulder' ]]; then
|
||||||
|
cli_args_arr+=(--network boulder_bluenet)
|
||||||
|
elif [[ "$ACME_CA" == 'pebble' ]]; then
|
||||||
|
cli_args_arr+=(--network acme_net)
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ "${DRY_RUN:-}" == 1 ]] && echo "Starting $container_name nginx container, with VIRTUAL_HOST=$virtual_host, LETSENCRYPT_HOST=$le_host and the following cli arguments : ${cli_args_arr[*]}."
|
||||||
|
|
||||||
|
if docker run --rm -d \
|
||||||
|
--name "${container_name:-$virtual_host}" \
|
||||||
|
-e "VIRTUAL_HOST=$virtual_host" \
|
||||||
|
-e "LETSENCRYPT_HOST=$le_host" \
|
||||||
|
--label com.github.jrcs.letsencrypt_nginx_proxy_companion.test_suite \
|
||||||
|
"${cli_args_arr[@]}" \
|
||||||
|
nginx:alpine > /dev/null ; \
|
||||||
|
then
|
||||||
|
[[ "${DRY_RUN:-}" == 1 ]] && echo "Started $container_name nginx container."
|
||||||
|
else
|
||||||
|
echo "Failed to start $container_name nginx container, with VIRTUAL_HOST=$virtual_host, LETSENCRYPT_HOST=$le_host and the following cli arguments : ${cli_args_arr[*]}."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
export -f run_nginx_container
|
||||||
|
|
||||||
|
|
||||||
|
# Wait for the /etc/nginx/conf.d/standalone-cert-$1.conf file to exist inside container $2
|
||||||
|
function wait_for_standalone_conf {
|
||||||
|
local domain="${1:?}"
|
||||||
|
local name="${2:?}"
|
||||||
|
local timeout
|
||||||
|
timeout="$(date +%s)"
|
||||||
|
timeout="$((timeout + 120))"
|
||||||
|
local target
|
||||||
|
until docker exec "$name" [ -f "/etc/nginx/conf.d/standalone-cert-$domain.conf" ]; do
|
||||||
|
if [[ "$(date +%s)" -gt "$timeout" ]]; then
|
||||||
|
echo "Standalone configuration file for $domain was not generated under one minute, timing out."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
}
|
||||||
|
export -f wait_for_standalone_conf
|
||||||
|
|
||||||
|
|
||||||
|
# Wait for the /etc/nginx/certs/$1.crt symlink to exist inside container $2
|
||||||
|
function wait_for_symlink {
|
||||||
|
local domain="${1:?}"
|
||||||
|
local name="${2:?}"
|
||||||
|
local expected_target="${3:-}"
|
||||||
|
local timeout
|
||||||
|
timeout="$(date +%s)"
|
||||||
|
timeout="$((timeout + 120))"
|
||||||
|
local target
|
||||||
|
until docker exec "$name" [ -L "/etc/nginx/certs/$domain.crt" ]; do
|
||||||
|
if [[ "$(date +%s)" -gt "$timeout" ]]; then
|
||||||
|
echo "Symlink for $domain certificate was not generated under one minute, timing out."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
[[ "${DRY_RUN:-}" == 1 ]] && echo "Symlink to $domain certificate has been generated."
|
||||||
|
if [[ -n "$expected_target" ]]; then
|
||||||
|
target="$(docker exec "$name" readlink "/etc/nginx/certs/$domain.crt")"
|
||||||
|
if [[ "$target" != "$expected_target" ]]; then
|
||||||
|
echo "The symlink to the $domain certificate is expected to point to $expected_target but point to $target instead."
|
||||||
|
return 1
|
||||||
|
elif [[ "${DRY_RUN:-}" == 1 ]]; then
|
||||||
|
echo "The symlink is pointing to the file $target"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
export -f wait_for_symlink
|
||||||
|
|
||||||
|
|
||||||
|
# Wait for the /etc/nginx/certs/$1.crt symlink to be removed inside container $2
|
||||||
|
function wait_for_symlink_rm {
|
||||||
|
local domain="${1:?}"
|
||||||
|
local name="${2:?}"
|
||||||
|
local timeout
|
||||||
|
timeout="$(date +%s)"
|
||||||
|
timeout="$((timeout + 120))"
|
||||||
|
until docker exec "$name" [ ! -L "/etc/nginx/certs/$domain.crt" ]; do
|
||||||
|
if [[ "$(date +%s)" -gt "$timeout" ]]; then
|
||||||
|
echo "Certificate symlink for $domain was not removed under one minute, timing out."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
[[ "${DRY_RUN:-}" == 1 ]] && echo "Symlink to $domain certificate has been removed."
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
export -f wait_for_symlink_rm
|
||||||
|
|
||||||
|
|
||||||
|
# Attempt to grab the certificate from domain passed with -d/--domain
|
||||||
|
# then check if the subject either match or doesn't match the pattern
|
||||||
|
# passed with either -m/--match or -nm/--no-match
|
||||||
|
# If domain can't be reached return 1
|
||||||
|
function check_cert_subj {
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
local flag="$1"
|
||||||
|
|
||||||
|
case $flag in
|
||||||
|
-d|--domain)
|
||||||
|
local domain="${2:?}"
|
||||||
|
shift
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
|
||||||
|
-m|--match)
|
||||||
|
local re="${2:?}"
|
||||||
|
local match_rc=0
|
||||||
|
local no_match_rc=1
|
||||||
|
shift
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
|
||||||
|
-n|--no-match)
|
||||||
|
local re="${2:?}"
|
||||||
|
local match_rc=1
|
||||||
|
local no_match_rc=0
|
||||||
|
shift
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
|
||||||
|
*) #Unknown option
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if curl -k https://"$domain" &> /dev/null; then
|
||||||
|
local cert_subject
|
||||||
|
cert_subject="$(echo \
|
||||||
|
| openssl s_client -showcerts -servername "$domain" -connect "$domain:443" 2>/dev/null \
|
||||||
|
| openssl x509 -subject -noout)"
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$cert_subject" =~ $re ]]; then
|
||||||
|
return $match_rc
|
||||||
|
else
|
||||||
|
return $no_match_rc
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
export -f check_cert_subj
|
||||||
|
|
||||||
|
|
||||||
|
# Wait for a successful https connection to domain passed with -d/--domain then wait
|
||||||
|
# - until the served certificate isn't the default one (default behavior)
|
||||||
|
# - until the served certificate is the default one (--default-cert)
|
||||||
|
# - until the served certificate subject match a string (--subject-match)
|
||||||
|
function wait_for_conn {
|
||||||
|
local action
|
||||||
|
local domain
|
||||||
|
local string
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
local flag="$1"
|
||||||
|
|
||||||
|
case $flag in
|
||||||
|
-d|--domain)
|
||||||
|
domain="${2:?}"
|
||||||
|
shift
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
|
||||||
|
--default-cert)
|
||||||
|
action='--match'
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
|
||||||
|
--subject-match)
|
||||||
|
action='--match'
|
||||||
|
string="$2"
|
||||||
|
shift
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
|
||||||
|
*) #Unknown option
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
local timeout
|
||||||
|
timeout="$(date +%s)"
|
||||||
|
timeout="$((timeout + 120))"
|
||||||
|
action="${action:---no-match}"
|
||||||
|
string="${string:-letsencrypt-nginx-proxy-companion}"
|
||||||
|
|
||||||
|
until check_cert_subj --domain "$domain" "$action" "$string"; do
|
||||||
|
if [[ "$(date +%s)" -gt "$timeout" ]]; then
|
||||||
|
echo "Could not connect to $domain using https under two minutes, timing out."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
[[ "${DRY_RUN:-}" == 1 ]] && echo "Connection to $domain using https was successful."
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
export -f wait_for_conn
|
||||||
|
|
||||||
|
|
||||||
|
# Get the expiration date in unix epoch of domain $1 inside container $2
|
||||||
|
function get_cert_expiration_epoch {
|
||||||
|
local domain="${1:?}"
|
||||||
|
local name="${2:?}"
|
||||||
|
local cert_expiration
|
||||||
|
cert_expiration="$(docker exec "$name" openssl x509 -noout -enddate -in "/etc/nginx/certs/$domain.crt")"
|
||||||
|
cert_expiration="$(echo "$cert_expiration" | cut -d "=" -f 2)"
|
||||||
|
if [[ "$(uname)" == 'Darwin' ]]; then
|
||||||
|
cert_expiration="$(date -j -f "%b %d %T %Y %Z" "$cert_expiration" "+%s")"
|
||||||
|
else
|
||||||
|
cert_expiration="$(date -d "$cert_expiration" "+%s")"
|
||||||
|
fi
|
||||||
|
echo "$cert_expiration"
|
||||||
|
}
|
||||||
|
export -f get_cert_expiration_epoch
|
||||||
1
test/tests/unit_tests/expected-std-out.txt
Normal file
1
test/tests/unit_tests/expected-std-out.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user