Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/cd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,27 @@ jobs:
secrets:
REGISTRY_USERNAME: ${{ github.actor }}
REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}

main4:
name: CD
needs:
- meta
- main1
uses:
./.github/workflows/docker_release.yml
with:
registry: ghcr.io
image: flobernd/haproxy-acme-tlsalpn01
context: ./haproxy-acme-tlsalpn01/data
build-args: |
BASE_IMAGE=ghcr.io/flobernd/haproxy-acme:${{ inputs.version-major }}.${{ inputs.version-minor }}
platforms: '["linux/386", "linux/amd64", "linux/arm/v5", "linux/arm/v7", "linux/arm64", "linux/mips64le", "linux/ppc64le", "linux/s390x"]'
tags: |
ghcr.io/flobernd/haproxy-acme-tlsalpn01:${{ inputs.version-major }}.${{ inputs.version-minor }}
ghcr.io/flobernd/haproxy-acme-tlsalpn01:${{ inputs.version-major }}
${{ inputs.tag-latest && format('{0}:latest', 'ghcr.io/flobernd/haproxy-acme-tlsalpn01') || '' }}
labels: ${{ needs.meta.outputs.labels }}
annotations: ${{ needs.meta.outputs.annotations }}
secrets:
REGISTRY_USERNAME: ${{ github.actor }}
REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
29 changes: 29 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ env:
REGISTRY_IMAGE_HAPROXY_ACME: localhost:5000/flobernd/haproxy-acme
REGISTRY_IMAGE_HAPROXY_ACME_HTTP01: localhost:5000/flobernd/haproxy-acme-http01
REGISTRY_IMAGE_HAPROXY_ACME_DNS01: localhost:5000/flobernd/haproxy-acme-dns01
REGISTRY_IMAGE_HAPROXY_ACME_TLSALPN01: localhost:5000/flobernd/haproxy-acme-tlsalpn01

jobs:
build:
Expand Down Expand Up @@ -120,3 +121,31 @@ jobs:
tags: ${{ steps.meta-haproxy-acme-dns01.outputs.tags }}
labels: ${{ steps.meta-haproxy-acme-dns01.outputs.labels }}
annotations: ${{ steps.meta-haproxy-acme-dns01.outputs.annotations }}

- name: Extract metadata for 'haproxy-acme-tlsalpn01'
id: meta-haproxy-acme-tlsalpn01
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE_HAPROXY_ACME_TLSALPN01 }}
# flavor: |
# latest=false
# tags: |
# type=ref,event=branch
# type=ref,event=pr
# type=semver,pattern={{version}}
# type=semver,pattern={{major}}.{{minor}}

- name: Build 'haproxy-acme-tlsalpn01'
id: build-haproxy-acme-tlsalpn01
uses: docker/build-push-action@v5
with:
context: ./haproxy-acme-tlsalpn01/data
cache-from: type=gha
cache-to: type=gha,mode=max
push: true
# platforms: linux/386,linux/amd64,linux/arm/v5,linux/arm/v7,linux/arm64,linux/mips64le,linux/ppc64le,linux/s390x
build-args: |
BASE_IMAGE=${{ fromJSON(steps.build-haproxy-acme.outputs.metadata)['image.name'] }}
tags: ${{ steps.meta-haproxy-acme-tlsalpn01.outputs.tags }}
labels: ${{ steps.meta-haproxy-acme-tlsalpn01.outputs.labels }}
annotations: ${{ steps.meta-haproxy-acme-tlsalpn01.outputs.annotations }}
140 changes: 140 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,146 @@ Optional directives for communicating with the internal service. For example `ss

---

## `haproxy-acme-tlsalpn01`

The `haproxy-acme-tlsalpn01` image is a ready-to-run image for local SSL termination and has the following core features:

- Issues a SSL certificate on startup
- Configurable ACME provider (`Let's Encrypt`, `ZeroSSL`, ...)
- Configurable key length (`2048`, `4096`, `ec-256`, ...)
- Supports SAN certificates
- Automatic certificate renewal
- HAProxy [Hitless Reload](https://www.haproxy.com/blog/hitless-reloads-with-haproxy-howto) (zero downtime)
- Configurable certificate renewal notifications (WiP)
- Support for [zero downtime TLS-ALPN authentication](https://github.com/acmesh-official/acme.sh/wiki/TLS-ALPN-without-downtime)

### Example

```bash
docker run -d --name haproxy-acme-tlsalpn01 \
-e "HAPROXY_HTTP_PORT=80" \
-e "HAPROXY_HTTPS_PORT=443" \
-e "HAPROXY_HTTPS_REASSIGN_PORT=8443" \
-e "ACME_MAIL=mail@domain.com" \
-e "ACME_DOMAIN=domain.com" \
-e "ACME_TLSALPN_PORT=10443" \
-v /docker_data/acme:/var/lib/acme:rw \
-p 80:80 \
-p 443:443 \
ghcr.io/flobernd/haproxy-acme-tlsalpn01
```

### Docker Compose Example

```yaml
services:
haproxy-acme:
image: ghcr.io/flobernd/haproxy-acme-tlsalpn01:latest
container_name: haproxy-acme-tlsalpn01
restart: unless-stopped
environment:
- "HAPROXY_HTTP_PORT=80"
- "HAPROXY_HTTPS_PORT=443"
- "HAPROXY_HTTPS_REASSIGN_PORT=8443"
- "ACME_MAIL=mail@domain.com"
- "ACME_DOMAIN=domain.com"
- "ACME_TLSALPN_PORT=10443"
volumes:
- /docker_data/acme:/var/lib/acme:rw
ports:
- 80:80
- 443:443
```

### Persistence

It is strongly recommended to specify an external volume for the `/var/lib/acme` directory. Most ACME servers enforce a rate limit for issuing and renewing certificates. If you recreate the container without preserving the internal state of `acme.sh`, a new certificate will also be created each time.

### HAProxy Configuration

The container creates a default configuration file `haproxy.cfg` in the `/usr/local/etc/haproxy` directory.

By mapping the aforementioned path, the primary `haproxy.cfg` can be freely customized. Alternatively, additional configurations can be placed in the `include` directory, which are then loaded after the primary configuration in alphabetical order.

As long as the default config has not been modified or overwritten, the `SERVER_ADDRESS` (*required*), `SERVER_PORT` (default `80`), `HAPROXY_HTTP_PORT` (default `80`), `HAPROXY_HTTPS_PORT` (default `443`), `HAPROXY_HTTPS_REASSIGN_PORT` (default `8443`) environment variables must be set. Otherwise the container will fail to start.

> [!IMPORTANT]
> When overwriting the default configuration, make sure that the `stats socket` directive is retained. Otherwise, the deployment of certificates will fail.

```
global
# Allow 'acme.sh' to deploy new certificates without reloading
stats socket /var/lib/haproxy/admin.sock level admin mode 660
```

### Environment

#### `ACME_DEBUG`

Set to `1` in order to enable verbose `acme.sh` debug output.

#### `ACME_UPGRADE`

Set to `1` in order to automatically update `acme.sh` to the latest version on container startup. This requires an active internet connection (default: `0`).

#### `ACME_CRON`

Set to `1` in order to enable the certificate renewal cronjob (default: `1`).

#### `ACME_SERVER`

The ACME server to use (default: `letsencrypt`).

Supported values: `letsencrypt`, `letsencrypt_test`, `buypass`, `buypass_test`, `zerossl`, `sslcom`, `google`, `googletest` or an explicit ACME server directory URL like e.g. `https://acme-v02.api.letsencrypt.org/directory`

See also: https://github.com/acmesh-official/acme.sh/wiki/Server

#### `ACME_MAIL`

The mail address for the ACME account registration (*required*).

#### `ACME_DOMAIN`

The domain to issue the certificate for (*required*). To issue a multi-domain certificate (SAN), enter additional domains separated by a space character after the primary domain.

For example: `sub.domain.com` (single), `domain1.com domain2.com` (SAN)

#### `ACME_KEYLENGTH`

The desired domain key length (default: `ec-256`).

Supported values (depending on the ACME server capabilities): `2048`, `3072`, `4096`, `8192`, `ec-256`, `ec-384`, `ec-521`.

#### `ACME_TLSALPN_PORT`

The port on which the internal stateful `acme.sh` server should run to handle the `acme-tls/1` request (default: `10443`).

#### `HAPROXY_HTTP_PORT`

The internal `haproxy` HTTP listening port. Allows changing the internal port to a non-privileged one (default: `80`). Do not forget to adjust the Docker port mapping accordingly (e.g. `-p 80:8080`).

#### `HAPROXY_HTTPS_PORT`

The internal `haproxy` HTTPS listening port. Allows changing the internal port to a non-privileged one (default: `443`). Do not forget to adjust the Docker port mapping accordingly (e.g. `-p 443:8443`).

#### `HAPROXY_HTTPS_REASSIGN_PORT`

The internal `haproxy` port to which the other frontends listening on `443` are reassigned (default: `8443`). The frontend listening on `443` instead proxies requests to this frontend or the `acme.sh` server.

#### `SERVER_ADDRESS`

The hostname or IP-address of the internal service for which SSL terminiation should be provided (*required*).

#### `SERVER_PORT`

The port of the internal service for which SSL terminiation should be provided (default: `80`).

#### `SERVER_DIRECTIVES`

Optional directives for communicating with the internal service. For example `ssl` must be specified here, if the internal service uses HTTPS. Check the `haproxy` documentation for more directives.

---

## `haproxy-acme`

The base image `haproxy-acme` is based on the [Docker "Official Image"](https://github.com/docker-library/haproxy) for [haproxy](https://www.haproxy.org/) and the [acme.sh](https://github.com/acmesh-official/acme.sh) Bash script. It serves as a generic template, providing some hook points for customization:
Expand Down
6 changes: 6 additions & 0 deletions haproxy-acme-tlsalpn01/attach.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash

docker exec \
-it \
haproxy-acme-tlsalpn01 \
/bin/bash
7 changes: 7 additions & 0 deletions haproxy-acme-tlsalpn01/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/bash

SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)

docker build \
-t flobernd/haproxy-acme-tlsalpn01:latest \
"$SCRIPT_DIR/data"
5 changes: 5 additions & 0 deletions haproxy-acme-tlsalpn01/build_and_run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash

set -e

./build.sh && ./run.sh
5 changes: 5 additions & 0 deletions haproxy-acme-tlsalpn01/build_and_run_compose.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash

set -e

./build.sh && docker compose up && docker compose down
33 changes: 33 additions & 0 deletions haproxy-acme-tlsalpn01/data/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
ARG BASE_IMAGE=flobernd/haproxy-acme:latest

FROM $BASE_IMAGE

# Environment

ENV HAPROXY_HTTP_PORT=80
ENV HAPROXY_HTTPS_PORT=443
ENV HAPROXY_HTTPS_REASSIGN_PORT=8443

ENV ACME_SERVER=letsencrypt
ENV ACME_MAIL=
ENV ACME_DOMAIN=
ENV ACME_KEYLENGTH=ec-256
ENV ACME_TLSALPN_PORT=10433

ENV SERVER_ADDRESS=
ENV SERVER_PORT=80
ENV SERVER_DIRECTIVES=

# Set up 'haproxy' certificate directory

RUN mkdir -p /etc/haproxy/certs && chown haproxy:haproxy /etc/haproxy/certs && chmod 0700 /etc/haproxy/certs

# Copy 'haproxy' configuration template

COPY --chown=haproxy:haproxy ./haproxy.cfg /etc/haproxy/haproxy.cfg.template

# Copy scripts

COPY acmeinit.early.sh /usr/local/bin/
COPY acmeinit.late.sh /usr/local/bin/
COPY initialize.sh /usr/local/bin/
35 changes: 35 additions & 0 deletions haproxy-acme-tlsalpn01/data/acmeinit.early.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/bin/bash
set -e

echo "Registering ACME account for '$ACME_MAIL' on '$ACME_SERVER' ..."

args=(
"--server" "$ACME_SERVER"
"--register-account"
"-m" "$ACME_MAIL"
)

if [ $ACME_DEBUG -eq 1 ]; then
args+=("--debug")
fi

input=$(acme.sh "${args[@]}") || (echo "$input" && exit 1)

pattern="^.*ACCOUNT_THUMBPRINT='([a-zA-Z0-9_-]+)'$"

if [[ $input =~ $pattern ]]; then
export ACME_ACCOUNT_THUMBPRINT="${BASH_REMATCH[1]}"

if [[ $input == *"Already registered"* ]]; then
echo "Already registered"
else
echo "Account created"
fi

echo "ACME account thumbprint:"
echo "$ACME_ACCOUNT_THUMBPRINT"
else
echo "Failed to extract ACME account thumbprint:"
echo "$input"
exit 1
fi
52 changes: 52 additions & 0 deletions haproxy-acme-tlsalpn01/data/acmeinit.late.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/bin/bash
set -e
set -f

echo "Issuing certificate for '$ACME_DOMAIN' ..."

domains=()
for d in $ACME_DOMAIN
do
domains+=("$d")
done

args=(
"--issue"
"--server" "$ACME_SERVER"
"--keylength" "$ACME_KEYLENGTH"
"--alpn"
"--tlsport" "$ACME_TLSALPN_PORT"
"-d" "${domains[0]}"
)

for d in "${domains[@]:1}"
do
args+=("-d" "$d")
done

if [ $ACME_DEBUG -eq 1 ]; then
args+=("--debug")
fi

result=0
acme.sh "${args[@]}" || result=$?

if [ $result -ne 0 ] && [ $result -ne 2 ]; then
# 0 = Certificate issued
# 2 = Certificate is still valid and does not require renewal
exit $result
fi

echo "Deploying certificate ..."

export DEPLOY_HAPROXY_HOT_UPDATE=yes
export DEPLOY_HAPROXY_STATS_SOCKET=/var/lib/haproxy/admin.sock
export DEPLOY_HAPROXY_PEM_PATH=/etc/haproxy/certs

args=(
"--deploy"
"--deploy-hook" "haproxy"
"-d" "${domains[0]}"
)

acme.sh "${args[@]}"
Loading