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
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,14 @@ RUN apk del tar && \
rm -f /var/cache/apk/*

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

ADD cli.ini /root/.config/letsencrypt/
ADD entrypoint.sh /
RUN chmod +x /entrypoint.sh

HEALTHCHECK --interval=5s --timeout=3s --start-period=5s --retries=10 CMD curl --fail --silent http://127.0.0.1/docker-health || exit 1
HEALTHCHECK --interval=5s --timeout=3s --start-period=5s --retries=10 CMD curl --fail --silent "http://127.0.0.1:${HTTP_PORT}/docker-health" || exit 1

RUN chown -R haproxy:haproxy /etc/haproxy

Expand Down
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ requested (this is a multi-value alternative to DOMAINNAME)
* `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 send to the Manager/Keycloak (default: `%[dst_port]` this is the HAProxy port)
* `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`)
Expand All @@ -33,6 +33,7 @@ requested (this is a multi-value alternative to DOMAINNAME)
* `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`)
* `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`
* `MQTT_RATE_LIMIT` - Enable rate limiting for MQTT connections (connections/s)
Expand All @@ -57,3 +58,21 @@ If you use an Ingress, reconfigure the `HTTPS_FORWARDED_PORT` to the HTTPS port

You will also need to set the `NAMESERVER` environment variable to the cluster DNS (usually 10.96.0.10:53).
The cluster DNS typically only resolves fully qualified hostnames, so make sure to set these using the `MANAGER_HOST` and `KEYCLOAK_HOST` environment variables (e.g. `manager.default.svc.cluster.local`).

## Edge-Terminated TLS

If TLS is terminated upstream before traffic reaches this pod, for example by an AWS NLB with ACM, an ALB, an ingress controller, or another reverse proxy, then:

* Set `DISABLE_ACME=true` to disable certbot initialization and renewal in the container
* Use `HAPROXY_CONFIG=/etc/haproxy/haproxy-edge-terminated-tls.cfg`
* Set `HTTP_PORT` to a non-privileged container port such as `8080`
* Set `HTTPS_FORWARDED_PORT=443` so upstream services see the original external HTTPS port
* Configure the upstream load balancer or proxy to forward decrypted HTTP traffic to the pod `HTTP_PORT`

For MQTT in the same setup, if MQTT TLS is also terminated upstream:

* Terminate TLS on the upstream listener (for example external port `8883`)
* Forward plaintext TCP traffic from that listener to the pod's MQTT port
* 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.
59 changes: 42 additions & 17 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ fi

LE_CMD="certbot certonly -n --logs-dir - -w ${CHROOT_DIR} ${LE_EXTRA_ARGS}"

acme_enabled() {
case "${DISABLE_ACME}" in
1|true|TRUE|yes|YES|on|ON)
return 1
;;
*)
return 0
;;
esac
}

# Configure haproxy
HAPROXY_CMD="haproxy -W -db -f ${HAPROXY_CONFIG} ${HAPROXY_USER_PARAMS}"
HAPROXY_RESTART_CMD="kill -s HUP 1"
Expand Down Expand Up @@ -73,21 +84,26 @@ run_proxy() {
log_info "AWS_ROUTE53_ROLE: ${AWS_ROUTE53_ROLE}"

if check_proxy; then

log_info "Starting crond"
crond

if [ -n "${AWS_ROUTE53_ROLE}" ]; then
log_info "Creating AWS CLI config file"
mkdir ~/.aws
rm -f ~/.aws/config 2> /dev/null
echo "[default]" >> ~/.aws/config
echo "role_arn = ${AWS_ROUTE53_ROLE}" >> ~/.aws/config
echo "credential_source = Ec2InstanceMetadata" >> ~/.aws/config
echo "" >> ~/.aws/config
start_monitor

if acme_enabled; then
log_info "Starting crond"
crond

if [ -n "${AWS_ROUTE53_ROLE}" ]; then
log_info "Creating AWS CLI config file"
mkdir ~/.aws
rm -f ~/.aws/config 2> /dev/null
echo "[default]" >> ~/.aws/config
echo "role_arn = ${AWS_ROUTE53_ROLE}" >> ~/.aws/config
echo "credential_source = Ec2InstanceMetadata" >> ~/.aws/config
echo "" >> ~/.aws/config
fi

cert_init&
else
log_info "ACME is disabled; skipping certbot initialization and renewal"
fi

cert_init&

log_info "HAProxy starting"
exec su haproxy -s /bin/sh -c "$HAPROXY_CMD $HAPROXY_START_OPTIONS"
Expand Down Expand Up @@ -129,6 +145,14 @@ restart() {
fi
}

start_monitor() {
log_info "Starting monitoring process"
(
sleep 5
monitor
)&
}

add() {
if [ $# -lt 1 ]
then
Expand Down Expand Up @@ -234,6 +258,10 @@ renew() {
}

auto_renew() {
if ! acme_enabled; then
log_info "ACME is disabled; skipping auto renew"
return 0
fi
log_info "Executing auto renew at $(date -R)"
certbot renew --deploy-hook "/entrypoint.sh sync-haproxy"
}
Expand Down Expand Up @@ -390,9 +418,6 @@ cert_init() {
log_info "HAProxy certs have been modified so restarting"
restart
fi

log_info "Starting monitoring process"
monitor
}

sync_haproxy() {
Expand Down
95 changes: 95 additions & 0 deletions haproxy-edge-terminated-tls.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#
# Use this config when TLS is terminated externally before traffic reaches
# this pod. HAProxy receives plain HTTP and plain TCP MQTT.
#
global
log stdout format raw local0 "${PROXY_LOGLEVEL}"

tune.ssl.default-dh-param 4096
# Works around breaking change in docker 23+ - just uses the old docker default value
fd-hard-limit 1048576

defaults
log global
mode http
log-format "%T %ft %ci:%cp %s %TR/%Tw/%Tc/%Tr/%Ta %{+Q}r %ST %ac/%fc/%bc/%sc/%rc %sq/%bq"
timeout connect 30s
timeout client 60s
timeout server 60s
timeout tunnel 720m
# never fail on address resolution
default-server init-addr none

resolvers docker_resolver
nameserver dns "${NAMESERVER}"

frontend stats
bind *:8404
http-request use-service prometheus-exporter if { path /metrics }
stats enable
stats uri /stats
stats refresh 10s

frontend http
bind *:"${HTTP_PORT}"

# Static health endpoint for docker healthcheck (don't log it)
acl url_docker_health path /docker-health
http-request set-log-level silent if url_docker_health
http-request return status 200 if url_docker_health

# TLS was already terminated upstream, so propagate the original scheme.
option forwardfor
http-request add-header X-Forwarded-Proto https
http-request set-header X-Forwarded-Host %[req.hdr(Host)]
http-request add-header X-Forwarded-Port "${HTTPS_FORWARDED_PORT}"
http-response add-header Strict-Transport-Security max-age=15768000
http-response add-header X-Robots-Tag noindex

# Gateway tunnelling config
.if defined(SISH_HOST) && defined(SISH_PORT)
acl gateway_sub_domain hdr_beg(host) gw-
use_backend sish if gateway_sub_domain
.endif

acl auth path_beg "${KEYCLOAK_PATH_PREFIX}/auth"
use_backend keycloak_backend if auth

use_backend manager_backend

listen mqtt
bind *:"${MANAGER_MQTT_PORT}"
mode tcp

.if defined(MQTT_RATE_LIMIT)
# Rate limiting
acl too_fast fe_sess_rate ge "${MQTT_RATE_LIMIT}"
tcp-request connection reject if too_fast
.endif

option clitcpka
timeout client 3h
timeout server 3h
option logasap
log-format "%T %ft CLIENT=%ci:%cp BACKEND=%bi:%bp %ts %ac/%fc/%bc/%sc/%rc %sq/%bq"
balance leastconn

server manager "${MANAGER_HOST}":"${MANAGER_MQTT_PORT}" resolvers docker_resolver

backend manager_backend
compression algo gzip deflate
compression type text/html text/css application/javascript application/json image/svg+xml
compression offload
server manager "${MANAGER_HOST}":"${MANAGER_WEB_PORT}" resolvers docker_resolver
.if defined(MANAGER_PATH_PREFIX)
http-request replace-path ^"${MANAGER_PATH_PREFIX}"(/.*)?$ \1
.endif

backend keycloak_backend
server keycloak "${KEYCLOAK_HOST}":"${KEYCLOAK_PORT}" resolvers docker_resolver

# Gateway tunnelling config
.if defined(SISH_HOST) && defined(SISH_PORT)
backend sish
server sish "${SISH_HOST}":"${SISH_PORT}" resolvers docker_resolver
.endif