Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
e8b4d34
ACME accounts can use your own CA: a directory URL and a CA certifica…
avandeputte Nov 9, 2026
fb39fb3
2.2.0: ACME accounts can use your own CA, such as acme2certifier in f…
avandeputte Nov 9, 2026
ee0c6f9
2.2.0: the CA list follows acme.sh, and renewal follows acme.sh's plan
avandeputte Nov 9, 2026
2a05e62
2.2.0: certificates from Active Directory Certificate Services, throu…
avandeputte Nov 9, 2026
7cb1462
Merge main (2.1.1) into feat/custom-acme-server
avandeputte Nov 10, 2026
8e7bdcb
2.2.0: review fixes for AD CS and renewal -- no code-loading settings…
avandeputte Nov 10, 2026
a5a769c
add support for acme.sh --challenge-alias
GGeorggg Nov 10, 2026
e6997f2
2.2.0: every node installs acme2certifier when the service starts, no…
avandeputte Nov 10, 2026
bd5cc71
DNS alias mode: aliases checked where they are saved, against the cer…
avandeputte Nov 10, 2026
0be4b01
Merge DNS alias mode (feat/challenge-alias) into the 2.2 beta
avandeputte Nov 10, 2026
6d770e2
2.2.0-beta.5: DNS alias mode
avandeputte Nov 10, 2026
ef75e1e
2.2.0: no EAB rows for Let's Encrypt, which has no EAB -- hidden in t…
avandeputte Nov 10, 2026
532cf71
2.2.0: 36 more recipes -- Exchange, AD FS, RD Gateway, SQL Server, Sy…
avandeputte Nov 11, 2026
bdf5745
Merge branch 'feat/custom-acme-server' into beta
avandeputte Nov 11, 2026
9da702e
2.2.0-beta.6: 36 more recipes, no EAB rows for Let's Encrypt
avandeputte Nov 11, 2026
78cee1f
2.2.0: certificates from your own CA, Active Directory Certificate Se…
avandeputte Nov 11, 2026
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
36 changes: 24 additions & 12 deletions .github/workflows/upstream.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
name: Check upstream versions

# The container image pins two things that move on their own schedule: the
# HAProxy branch it installs from haproxy.debian.net (ARG HAPROXY_BRANCH) and
# the acme.sh release it and install.sh carry (ARG ACME_VERSION). Debian's own
# packages need no watching -- the weekly image rebuild picks them up -- but
# a new HAProxy LTS or an acme.sh release would otherwise go unnoticed.
# The container image pins three things that move on their own schedule: the
# HAProxy branch it installs from haproxy.debian.net (ARG HAPROXY_BRANCH), the
# acme.sh release it and install.sh carry (ARG ACME_VERSION), and the
# acme2certifier release it and ham/adcs.py carry (ARG A2C_VERSION). Debian's
# own packages need no watching -- the weekly image rebuild picks them up --
# but a new HAProxy LTS or an acme.sh or acme2certifier release would
# otherwise go unnoticed.
#
# Once a week this compares the pins with what is out there and, when either
# Once a week this compares the pins with what is out there and, when any
# is behind, edits them and opens a pull request (or refreshes the one it
# already opened). The pull request builds and starts the image, so merging
# it is the only step left for a person.
Expand All @@ -33,21 +35,26 @@ jobs:
set -eu
have_haproxy=$(grep -oP '^ARG HAPROXY_BRANCH=\K[0-9.]+' Dockerfile)
have_acme=$(grep -oP '^ARG ACME_VERSION=\K[0-9.]+' Dockerfile)
have_a2c=$(grep -oP '^ARG A2C_VERSION=\K[0-9.]+' Dockerfile)
codename=$(grep -oP '^FROM debian:\K[a-z]+' Dockerfile)

# the newest branch haproxy.debian.net publishes for this Debian
want_haproxy=$(curl -fsSL https://haproxy.debian.net/dists/ \
| grep -oE "${codename}-backports-[0-9]+\.[0-9]+" | sed 's/.*-//' | sort -V | tail -1)
# the newest acme.sh release (releases, not tags: a tag may be a draft)
want_acme=$(gh api repos/acmesh-official/acme.sh/releases/latest --jq .tag_name)
# the newest acme2certifier release, which carries the .deb the image installs
want_a2c=$(gh api repos/grindsa/acme2certifier/releases/latest --jq .tag_name)

echo "HAProxy branch: pinned $have_haproxy, newest $want_haproxy"
echo "acme.sh: pinned $have_acme, newest $want_acme"
test -n "$want_haproxy" && test -n "$want_acme"
echo "HAProxy branch: pinned $have_haproxy, newest $want_haproxy"
echo "acme.sh: pinned $have_acme, newest $want_acme"
echo "acme2certifier: pinned $have_a2c, newest $want_a2c"
test -n "$want_haproxy" && test -n "$want_acme" && test -n "$want_a2c"
{
echo "have_haproxy=$have_haproxy"; echo "want_haproxy=$want_haproxy"
echo "have_acme=$have_acme"; echo "want_acme=$want_acme"
if [ "$have_haproxy" != "$want_haproxy" ] || [ "$have_acme" != "$want_acme" ]; then
echo "have_a2c=$have_a2c"; echo "want_a2c=$want_a2c"
if [ "$have_haproxy" != "$want_haproxy" ] || [ "$have_acme" != "$want_acme" ] || [ "$have_a2c" != "$want_a2c" ]; then
echo "behind=true"
else
echo "behind=false"
Expand All @@ -62,20 +69,25 @@ jobs:
WANT_HAPROXY: ${{ steps.upstream.outputs.want_haproxy }}
HAVE_ACME: ${{ steps.upstream.outputs.have_acme }}
WANT_ACME: ${{ steps.upstream.outputs.want_acme }}
HAVE_A2C: ${{ steps.upstream.outputs.have_a2c }}
WANT_A2C: ${{ steps.upstream.outputs.want_a2c }}
run: |
set -eu
sed -i "s/^ARG HAPROXY_BRANCH=.*/ARG HAPROXY_BRANCH=$WANT_HAPROXY/" Dockerfile
sed -i "s/^ARG ACME_VERSION=.*/ARG ACME_VERSION=$WANT_ACME/" Dockerfile
sed -i "s/^ACME_VERSION=\"\${HAM_ACME_VERSION:-[0-9.]*}\"/ACME_VERSION=\"\${HAM_ACME_VERSION:-$WANT_ACME}\"/" install.sh
sed -i "s/^ARG A2C_VERSION=.*/ARG A2C_VERSION=$WANT_A2C/" Dockerfile
sed -i "s/^A2C_VERSION = \"[0-9.]*\"/A2C_VERSION = \"$WANT_A2C\"/" ham/adcs.py
git diff --stat

lines=""
[ "$HAVE_HAPROXY" != "$WANT_HAPROXY" ] && lines="$lines- HAProxy branch $HAVE_HAPROXY -> $WANT_HAPROXY (haproxy.debian.net)\n"
[ "$HAVE_ACME" != "$WANT_ACME" ] && lines="$lines- acme.sh $HAVE_ACME -> $WANT_ACME, in the image and in install.sh\n"
title="Upstream: HAProxy $WANT_HAPROXY, acme.sh $WANT_ACME"
[ "$HAVE_A2C" != "$WANT_A2C" ] && lines="$lines- acme2certifier $HAVE_A2C -> $WANT_A2C, in the image and in ham/adcs.py\n"
title="Upstream: HAProxy $WANT_HAPROXY, acme.sh $WANT_ACME, acme2certifier $WANT_A2C"
body=$(printf 'Opened by the weekly upstream check.\n\n%b\nThe pull request builds the image and starts it. After merging: bump VERSION, and update the acme.sh default on the install page of the documentation (HAProxyManagerDocs, install-standalone.md).\n' "$lines")

branch="upstream/haproxy-$WANT_HAPROXY-acme-$WANT_ACME"
branch="upstream/haproxy-$WANT_HAPROXY-acme-$WANT_ACME-a2c-$WANT_A2C"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -B "$branch"
Expand Down
17 changes: 17 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ FROM debian:trixie-slim
# suite, for amd64 and arm64, and publishes each point release as it comes.
ARG HAPROXY_BRANCH=3.4
ARG ACME_VERSION=3.1.6
# acme2certifier, for certificates from Active Directory Certificate Services
# (ham/adcs.py pins the same release for nodes that install it on demand).
ARG A2C_VERSION=0.46.4

ENV DEBIAN_FRONTEND=noninteractive

Expand Down Expand Up @@ -62,6 +65,20 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
procps \
supervisor \
tzdata \
# acme2certifier: an ACME server in front of a CA that does not speak
# ACME, run by the app on localhost while a certificate is requested from
# Active Directory Certificate Services. Its .deb comes from its GitHub
# release, as acme.sh's tarball does; its dependencies are Debian's own,
# Impacket included, which MS-ICPR enrolls through. Impacket's package
# also carries example scripts (secretsdump and the like) that scanners
# flag and enrollment never uses: left out, as its own documentation
# recommends.
&& printf 'path-exclude /usr/bin/impacket-*\npath-exclude /usr/share/doc/python3-impacket/*\n' \
> /etc/dpkg/dpkg.cfg.d/zz-impacket \
&& curl -fsSL "https://github.com/grindsa/acme2certifier/releases/download/${A2C_VERSION}/acme2certifier_${A2C_VERSION}-1_all.deb" \
-o /tmp/acme2certifier.deb \
&& apt-get install -y --no-install-recommends /tmp/acme2certifier.deb python3-impacket \
&& rm -f /tmp/acme2certifier.deb \
&& rm -rf /var/lib/apt/lists/* \
&& cp /etc/keepalived/keepalived.config-opts /usr/sbin/keepalived.config-opts

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
<!-- /docs-site:skip -->

A small self-hosted web UI to manage an **HAProxy** configuration, obtain
**Let's Encrypt** certificates, and run a **cluster of any number of nodes with
**Let's Encrypt** certificates (or your own CA's, Active Directory Certificate
Services included), and run a **cluster of any number of nodes with
Keepalived** on a shared virtual IP — one node active, the rest ready to take
over, with settings and certificates syncing across all of them. Since 2.0 it
also keeps named **address lists**, lets a service carry its own **rules** and
Expand All @@ -22,7 +23,7 @@ copying it across.

```bash
# from a package: .deb and .rpm on every release (Debian, Ubuntu, RHEL, Fedora)
sudo apt-get install -y ./haproxy-manager_2.1.1_all.deb
sudo apt-get install -y ./haproxy-manager_2.2.0_all.deb

# or the install script, on any Debian-based server
curl -fsSL https://raw.githubusercontent.com/avandeputte/haproxy-manager/main/install.sh | sudo bash
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.1.1
2.2.0
3 changes: 2 additions & 1 deletion app.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
from ham.acme import _renew_loop
from ham.updates import _update_loop
from ham.watchdog import _watchdog_loop
from ham import crowdsec, sync, traffic
from ham import adcs, crowdsec, sync, traffic


def _cli(argv):
Expand Down Expand Up @@ -205,6 +205,7 @@ def _serve():
# The CrowdSec bouncer: put on this node if it is missing (a package
# install cannot fetch it), and brought up when CrowdSec is on.
threading.Thread(target=crowdsec.startup, daemon=True).start()
threading.Thread(target=adcs.startup, daemon=True).start()
# The traffic history is written every ten minutes, so stopping must write
# the rest. systemd and supervisord stop with SIGTERM, which by default
# ends Python without running anything registered here; made an ordinary
Expand Down
1 change: 1 addition & 0 deletions ham/FILES
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
__init__.py
access.py
acme.py
adcs.py
apply.py
auth.py
backup.py
Expand Down
3 changes: 2 additions & 1 deletion ham/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
metrics,
apply,
acme,
adcs,
notify,
watchdog,
mcp,
Expand Down Expand Up @@ -61,7 +62,7 @@
__all__ = [
"app", "log", "VERSION", "config", "util", "auth", "access", "twofactor",
"qr", "validate",
"crud", "haproxy", "history", "keepalived", "metrics", "apply", "acme", "notify",
"crud", "haproxy", "history", "keepalived", "metrics", "apply", "acme", "adcs", "notify",
"watchdog",
"sync", "setup", "peering", "probe", "vrrp", "cluster", "stats", "traffic",
"mqtt", "hass", "oauth", "logs",
Expand Down
Loading
Loading