Skip to content
Open
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
11 changes: 8 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
ENV LOGFILE=${LOGFILE}

ENV CERT_DIR=/deployment/certs
ENV CUSTOM_CERT_DIR=/data/proxy/certs
ENV LE_DIR=/deployment/letsencrypt
ENV CHROOT_DIR=/etc/haproxy/webroot

Expand All @@ -63,15 +64,17 @@
RUN apk update \
&& apk add --no-cache certbot curl inotify-tools openssl py-pip tar \
&& rm -f /var/cache/apk/* \
&& pip install certbot-dns-route53 --break-system-packages

Check warning on line 67 in Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Omitting "--only-binary :all:" can lead to the execution of setup scripts. Make sure it is safe here.

See more on https://sonarcloud.io/project/issues?id=openremote_proxy&issues=AZ4r3pKKzgvZm-J93WOA&open=AZ4r3pKKzgvZm-J93WOA&pullRequest=27

Check warning on line 67 in Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Using dependencies without locking resolved versions is security-sensitive.

See more on https://sonarcloud.io/project/issues?id=openremote_proxy&issues=AZ4r3pKKzgvZm-J93WOB&open=AZ4r3pKKzgvZm-J93WOB&pullRequest=27

# Add ACME LUA plugin
ADD acme-plugin.tar.gz /etc/haproxy/lua/

RUN mkdir -p "${CHROOT_DIR}" \
&& mkdir -p "${CERT_DIR}" \
&& mkdir -p "${CUSTOM_CERT_DIR}" \
&& mkdir -p /var/log/letsencrypt \
&& mkdir -p "${LE_DIR}" && chown haproxy:haproxy "${LE_DIR}" \
&& mkdir -p "${LE_DIR}" \
&& mkdir -p /etc/haproxy/certs \
&& mkdir -p /etc/letsencrypt \
&& mkdir -p /var/lib/letsencrypt \
&& touch /etc/periodic/daily/cert-renew \
Expand All @@ -82,14 +85,16 @@
&& chown -R haproxy:haproxy /var/lib/letsencrypt \
&& chown -R haproxy:haproxy /var/log/letsencrypt \
&& chown -R haproxy:haproxy "${CHROOT_DIR}" \
&& chown -R haproxy:haproxy "${CERT_DIR}"
&& chown -R haproxy:haproxy "${CERT_DIR}" \
&& chown -R haproxy:haproxy "${CUSTOM_CERT_DIR}" \
&& chown -R haproxy:haproxy "${LE_DIR}" \
&& chown -R haproxy:haproxy /etc/haproxy/certs

RUN apk del tar && \
rm -f /var/cache/apk/*

COPY haproxy.cfg /etc/haproxy/haproxy.cfg
COPY haproxy-edge-terminated-tls.cfg /etc/haproxy/haproxy-edge-terminated-tls.cfg
COPY certs /etc/haproxy/certs

COPY cli.ini /root/.config/letsencrypt/
COPY entrypoint.sh /
Expand Down
50 changes: 25 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,31 @@

HAProxy docker image with Lets Encrypt SSL auto renewal using certbot with built in support for wildcard certificates using AWS Route53.

## Paths

* `/deployment/letsencrypt` - Certbot config directory where generated certificates are stored
* `/etc/haproxy/haproxy.cfg` - Default location of haproxy configuration file
* `/etc/haproxy/certs` - Static (non certbot) certificates includes self-signed and any other static certificates should be volume mapped into this folder
* `/var/log/*` - Location of log files (all are symlinked to stdout)

## Environment variables

* `DOMAINNAME` - IANA TLD subdomain for which a Lets Encrypt certificate should be requested
* `CERT_DIR` - Automatically generated full chain PEM certificates directory (live reload of HA Proxy on changes) \[default: `/deployment/certs`\]
* `CUSTOM_CERT_DIR` - Additional custom full chain PEM certificates directory loaded by HAProxy but not managed by certbot \[default: `/data/proxy/certs`\]
* `LE_DIR` - Certbot config directory where generated certificates are stored \[default: `/deployment/letsencrypt`\]
* \[DEPRECATED\] `DOMAINNAME` - IANA TLD subdomain for which a Lets Encrypt certificate should be requested
* `DOMAINNAMES` - Comma separated list of IANA TLD subdomain names for which Lets Encrypt certificates should be
requested (this is a multi-value alternative to DOMAINNAME)
requested; wildcard domains should be specified with an '*' (e.g. `*.example.com)
* `HAPROXY_USER_PARAMS` - Additional arguments that should be passed to the haproxy process during startup
* `HAPROXY_CONFIG` - Location of HAProxy config file (default: `/etc/haproxy/haproxy.cfg`)
* `PROXY_LOGLEVEL` - Log level for HAProxy (default: `notice`)
* `HTTP_PORT` - The container binds to this port for handling HTTP requests (default: `80`)
* `HTTPS_PORT` - The container binds to this port for handling HTTPS requests (default: `443`)
* `HTTPS_FORWARDED_PORT` - The port set in the `X-Forwarded-Port` header of requests sent to the Manager/Keycloak (default: `%[dst_port]` this is the HAProxy port)
* `NAMESERVER` - The nameserver hostname and port used for resolving the Manager/Keycloak hosts (default: `127.0.0.11:53`)
* `MANAGER_HOST` - Hostname of OpenRemote Manager (default: `manager`)
* `MANAGER_WEB_PORT` - Web server port of OpenRemote Manager (default `8080`)
* `MANAGER_MQTT_PORT` - MQTT broker port of OpenRemote Manager (default `1883`)
* `MANAGER_PATH_PREFIX` - The path prefix used for OpenRemote Manager HTTP requests (default not set, example: `/openremote`)
* `KEYCLOAK_HOST` - Hostname of the Keycloak server (default: `keycloak`)
* `KEYCLOAK_PORT` - Web server port of Keycloak server (default `8080`)
* `KEYCLOAK_PATH_PREFIX` - The path prefix used for Keycloak HTTP requests (default not set, example: `/keycloak`)
* `LOGFILE` - Location of log file for entrypoint script to write to in addition to stdout (default `none`)
* `AWS_ROUTE53_ROLE` - AWS Route53 Role ARN to be assumed when trying to generate wildcard certificates using Route53 DNS zone, specifically for cross account updates (default `none`)
* `LE_EXTRA_ARGS` - Can be used to add additional arguments to the certbot command (default `none`)
* `HAPROXY_CONFIG` - Location of HAProxy config file (live reload of HA Proxy on changes) \[default: `/etc/haproxy/haproxy.cfg`\]
* `PROXY_LOGLEVEL` - Log level for HAProxy \[default: `notice`\]
* `HTTP_PORT` - The container binds to this port for handling HTTP requests \[default: `80`\]
* `HTTPS_PORT` - The container binds to this port for handling HTTPS requests \[default: `443`\]
* `HTTPS_FORWARDED_PORT` - The port set in the `X-Forwarded-Port` header of requests sent to the Manager/Keycloak \[default: `%[dst_port]` this is the HAProxy port\]
* `NAMESERVER` - The nameserver hostname and port used for resolving the Manager/Keycloak hosts \[default: `127.0.0.11:53`\]
* `MANAGER_HOST` - Hostname of OpenRemote Manager \[default: `manager`\]
* `MANAGER_WEB_PORT` - Web server port of OpenRemote Manager \[default: `8080`\]
* `MANAGER_MQTT_PORT` - MQTT broker port of OpenRemote Manager \[default: `1883`\]
* `MANAGER_PATH_PREFIX` - The path prefix used for OpenRemote Manager HTTP requests (e.g. `/openremote`) \[default: not set\]
* `KEYCLOAK_HOST` - Hostname of the Keycloak server \[default: `keycloak`\]
* `KEYCLOAK_PORT` - Web server port of Keycloak server \[default: `8080`\]
* `KEYCLOAK_PATH_PREFIX` - The path prefix used for Keycloak HTTP requests (e.g. `/keycloak`) \[default: not set\]
* `LOGFILE` - Location of log file for entrypoint script to write to in addition to stdout \[default: `none`\]
* `AWS_ROUTE53_ROLE` - AWS Route53 Role ARN to be assumed when trying to generate wildcard certificates using Route53 DNS zone, specifically for cross account updates \[default: not set\]
* `LE_EXTRA_ARGS` - Can be used to add additional arguments to the certbot command \[default: not set\]
* `DISABLE_ACME` - Disable certbot/ACME initialization and renewal logic in the entrypoint; useful when TLS is terminated externally such as with ACM on an AWS load balancer (accepted true values: `1`, `true`, `yes`, `on`)
* `SISH_HOST` - Defines the destination hostname for forwarding requests that begin with `gw-` used in combination with `SISH_PORT`
* `SISH_PORT` - Defined the destination port for forwarding requests tha begin with `gw-` used in combination with `SISH_HOST`
Expand Down Expand Up @@ -76,3 +72,7 @@ For MQTT in the same setup, if MQTT TLS is also terminated upstream:
* The provided `haproxy-edge-terminated-tls.cfg` listens for MQTT on `MANAGER_MQTT_PORT` and forwards it to the configured manager MQTT backend

The `haproxy-edge-terminated-tls.cfg` file removes local TLS certificate usage from the pod and preserves the usual `X-Forwarded-*` HTTP headers for upstream applications. Do not use this config if HTTPS or MQTT TLS is still passed through to the pod.

## Logs

* `/var/log/*` - Location of log files (all are symlinked to stdout)
50 changes: 0 additions & 50 deletions certs/01-selfsigned

This file was deleted.

48 changes: 40 additions & 8 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ HAPROXY_RESTART_CMD="kill -s HUP 1"
HAPROXY_CHECK_CONFIG_CMD="haproxy -f ${HAPROXY_CONFIG} -c"

# Make dirs and files
mkdir -p /deployment/letsencrypt/live
mkdir -p /deployment/certs
mkdir -p $LE_DIR/live
mkdir -p $CERT_DIR
mkdir -p $CUSTOM_CERT_DIR
mkdir -p /etc/haproxy/certs

if [ "$DOMAINNAME" == 'localhost' ]; then
# To maintain support for existing setups
Expand Down Expand Up @@ -79,10 +81,16 @@ run_proxy() {
log_info "PROXY_LOGLEVEL: ${PROXY_LOGLEVEL}"
log_info "LUA_PATH: ${LUA_PATH}"
log_info "CERT_DIR: ${CERT_DIR}"
log_info "CUSTOM_CERT_DIR: ${CUSTOM_CERT_DIR}"
log_info "LE_DIR: ${LE_DIR}"
log_info "LE_CMD: ${LE_CMD}"
log_info "AWS_ROUTE53_ROLE: ${AWS_ROUTE53_ROLE}"

ensure_selfsigned_cert

log_info "Custom certs:"
ls -al ${CUSTOM_CERT_DIR}

if check_proxy; then
start_monitor

Expand Down Expand Up @@ -123,10 +131,10 @@ run_proxy() {

monitor() {
while true; do
log_info "Monitoring config file '$HAPROXY_CONFIG' and certs in '$CERT_DIR' for changes..."
log_info "Monitoring config file '$HAPROXY_CONFIG' and certs in '/etc/haproxy/certs', '$CERT_DIR' and '$CUSTOM_CERT_DIR' for changes..."

# Wait if config or certificates were changed, block this execution
inotifywait -q -r --exclude '\.git/' -e modify,create,delete,move,move_self "$HAPROXY_CONFIG" "$CERT_DIR"
inotifywait -q -r --exclude '\.git/' -e modify,create,delete,move,move_self "$HAPROXY_CONFIG" "/etc/haproxy/certs" "$CERT_DIR" "$CUSTOM_CERT_DIR"
log_info "Change detected..." &&
sleep 5 &&
restart
Expand Down Expand Up @@ -260,6 +268,8 @@ renew() {
}

auto_renew() {
ensure_selfsigned_cert

if ! acme_enabled; then
log_info "ACME is disabled; skipping auto renew"
return 0
Expand Down Expand Up @@ -367,10 +377,6 @@ cert_init() {
rm -rf "${LE_DIR}/live/${FNAME}" 2>/dev/null
add "${DOMAIN}"
fi
if [ $i -eq 1 ]; then
log_info "Symlinking first domain to built in cert directory to take precedence over self signed cert"
ln -sfT ${CERT_DIR}/${FNAME} /etc/haproxy/certs/00-cert
fi
done
IFS=$IFS_OLD

Expand Down Expand Up @@ -440,6 +446,32 @@ sync_haproxy() {
return $?
}

ensure_selfsigned_cert() {
SELF_SIGNED_CERT="/etc/haproxy/certs/00-selfsigned"
mkdir -p /etc/haproxy/certs

# Check if self-signed cert exists and is valid for at least 30 more days (2592000 seconds)
if [ -f "$SELF_SIGNED_CERT" ]; then
if openssl x509 -checkend 2592000 -noout -in "$SELF_SIGNED_CERT" >/dev/null 2>&1; then
return 0
else
log_info "Self-signed certificate is expired or expiring within 30 days. Regenerating..."
fi
else
log_info "No self-signed HAProxy certificate found; generating one."
fi

# Generate a new certificate valid for 365 days
openssl req -x509 -nodes -newkey rsa:2048 -sha256 -days 365 \
-subj "/CN=localhost" \
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@richturner Shouldn't this be OR_HOSTNAME? From what I understand around the usage of OR_HOSTNAME, this function will be ran if OR_HOSTNAME is not an FQDN, so using localhost could block people from properly using it on other domain names.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pankalog no the purpose of the self signed certificate is just as a final fallback if SNI fails to match a specific cert, it is bad practice to present a valid public FQDN in this scenario, this just replaces the previous selfsigned cert that was baked in

-addext "subjectAltName=DNS:localhost,IP:127.0.0.1" \
-keyout /tmp/selfsigned.key \
-out /tmp/selfsigned.crt || return $?

cat /tmp/selfsigned.key /tmp/selfsigned.crt > "$SELF_SIGNED_CERT"
rm -f /tmp/selfsigned.key /tmp/selfsigned.crt
}

if [ $# -eq 0 ]
then
print_help
Expand Down
4 changes: 2 additions & 2 deletions haproxy.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ frontend http
redirect scheme https code 301 if !url_acme_http01 !url_docker_health

frontend https
bind *:"${HTTPS_PORT}" ssl crt /etc/haproxy/certs crt "${CERT_DIR}" no-tls-tickets
bind *:"${HTTPS_PORT}" ssl crt /etc/haproxy/certs crt "${CERT_DIR}" crt "${CUSTOM_CERT_DIR}" no-tls-tickets

# Optional: redirects for root requests with certain host names to service paths
acl is_root path -i /
Expand Down Expand Up @@ -123,7 +123,7 @@ frontend https
use_backend manager_backend

listen mqtt
bind *:8883 ssl crt /etc/haproxy/certs crt "${CERT_DIR}" no-tls-tickets
bind *:8883 ssl crt /etc/haproxy/certs crt "${CERT_DIR}" crt "${CUSTOM_CERT_DIR}" no-tls-tickets
mode tcp

.if defined(MQTT_RATE_LIMIT)
Expand Down
Loading