Skip to content

Commit e7c00d1

Browse files
committed
feat(haproxy-status)!: switch HTTP basic auth to URL-embedded credentials
BREAKING CHANGE: --username and --password were removed in favour of HTTP basic auth embedded in the URL itself, e.g. --url=https://stats:s3cret@webserver:8443/server-status The plugin strips the credentials from the netloc before the request is sent and instead carries them in an "Authorization: Basic" header, so they never reach the request line or any proxy access log. Both flags stay registered in argparse with argparse.SUPPRESS so we can detect legacy usage and exit UNKNOWN with a clear migration pointer instead of letting parse_known_args silently swallow them and surface a confusing 401 from the haproxy stats endpoint. The base64-and-Authorization-header dance is now delegated to lib.url.split_basic_auth(), the same helper php-fpm-status already uses (newly hoisted from a local copy in php-fpm-status into lib.url in linuxfabrik-lib 3.2.0). Bump the linuxfabrik-lib pin from 3.1.1 to 3.2.0 to pick that helper up. Existing Icinga services that still pass --username / --password through service templates need to migrate the URL field to include the credentials and drop the username/password fields. The Icinga Director basket needs a regen to drop the now-removed fields. Two new unit tests cover the legacy-flag UNKNOWN guard.
1 parent 1d93d57 commit e7c00d1

File tree

6 files changed

+114
-56
lines changed

6 files changed

+114
-56
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ Build, CI/CD:
1616

1717
Monitoring Plugins:
1818

19+
* haproxy-status: `--username` and `--password` were removed in favour of HTTP basic auth embedded in the URL itself, e.g. `--url=https://stats:s3cret@webserver:8443/server-status`. The plugin strips the credentials from the netloc before the request is sent and instead carries them in an `Authorization: Basic` header so they never reach the request line or any proxy access log. Existing Icinga services that still pass `--username` / `--password` will hard-UNKNOWN with a clear migration pointer; migrate the URL field to include the credentials and drop the username/password fields from the service template
1920
* mailq: the alerting semantic has flipped from "number of mails in the queue" to "age of the oldest mail in the queue". `--warning` and `--critical` now take a duration string with a unit suffix (`1h`, `3D`, `30m`, `72h`, ...) and the defaults are `1h` / `3D` (down from `2` / `250` mails). The rationale is that a queue with 100 fresh mails is still OK when they are delivered within minutes, while a single mail stuck for more than an hour is always interesting and is exactly when an admin wants to look. The mail count stays in perfdata (`mailq`) so Grafana trending keeps working, and the new `oldest_mail_age` perfdata metric carries the age in seconds. Existing Icinga services that set `mailq_warning=10` and `mailq_critical=500` need to be migrated to duration strings (e.g. `mailq_warning=1h`, `mailq_critical=3D`). Also adds `--mta=auto|postfix|exim|sendmail` to override MTA autodetection: Postfix is now read via `postqueue -j` (JSON with `arrival_time` as Unix epoch) for a rock-solid timestamp, Exim still uses `mailq` (= `exim -bp`) and its built-in age literals, and everything else falls back to `mailq` with `Date:` line parsing ([#781](https://github.com/Linuxfabrik/monitoring-plugins/issues/781))
21+
* php-fpm-status: multi-pool support plus full rework of the alerting and perfdata semantics. `--url` can now be specified multiple times to check several pools on the same host in a single plugin run; the overall state is the worst of all pools, and the summary line calls out which pool is in trouble. Basic authentication can be embedded in the URL as `http://user:password@host/pool-status`, with the credentials stripped from the request URL and sent via an `Authorization` header. All perfdata labels are now prefixed with the pool name (for example `nextcloud saturation` instead of `saturation`), including in the single-pool case, so existing Grafana panels and InfluxDB queries need to be updated; the bundled Grafana dashboard has been rewritten to use a `$pool` template variable that lets the admin switch between pools. The wrongly-named `queue usage` metric has been renamed to `saturation` and now correctly reports the percentage of busy worker processes (`active_processes / total_processes`) instead of the socket backlog fill rate, which was never "pool near capacity". The cumulative counters `accepted conn` and `slow requests` have been replaced by the computed `accepted conn rate` (requests per second since the previous run) and `slow requests delta` (new slow requests since the previous run), which follow the "no continuous counters" rule in CONTRIBUTING and make the `--warning-slowreq` / `--critical-slowreq` thresholds actually actionable again (previously they alerted permanently after the first historical slow request, until the next FPM restart). The `max children reached` metric and its implicit WARN have been removed and will return with delta-based semantics in a later release. A new `--severity warn|crit` parameter controls the state reported for pools that are unreachable or whose status JSON cannot be parsed; the default is `warn`. The `--lengthy` flag now correctly defaults to off (matching the project convention) and adds extended columns to both the pool overview table and the per-process table. The plugin uses a local SQLite cache (`linuxfabrik-monitoring-plugins-php-fpm-status.db`) to compute deltas between runs; the first run after install, and the first run after an FPM restart, report OK for the delta metrics with a `baseline captured, waiting for more data` note while the saturation metric is alerted normally. Unit tests now run against real PHP-FPM status JSON captured from four container platforms (UBI8 + PHP 7.2, UBI9 + PHP 8.0, Debian 12 + PHP 8.2, Ubuntu 24.04 + PHP 8.3)
2022
* procs: `--argument`, `--command` and `--username` now use regular expressions instead of substring/startswith matching. Existing filters like `--command=httpd` still work but now match anywhere in the name. Use `--command='^httpd'` for the previous startswith behavior, or `--username='^apache$'` for exact matches.
2123

2224

@@ -71,7 +73,7 @@ Assets:
7173

7274
Build, CI/CD:
7375

74-
* Bump pinned `linuxfabrik-lib` dependency from 3.0.0 to 3.1.1, picking up the new `run_mariadb()` / `MARIADB_LTS_IMAGES` container-test helpers, the `attach_each()` / `attach_tests()` unit-test helpers, the `disk.dir_exists()` directory check, and `human2seconds()` / `humanduration2seconds()` accepting the lowercase `d` / `w` day/week markers (which the mailq plugin needs to parse exim age literals)
76+
* Bump pinned `linuxfabrik-lib` dependency from 3.0.0 to 3.2.0, picking up the new `run_mariadb()` / `MARIADB_LTS_IMAGES` container-test helpers, the `attach_each()` / `attach_tests()` unit-test helpers, the `disk.dir_exists()` directory check, `human2seconds()` / `humanduration2seconds()` accepting the lowercase `d` / `w` day/week markers (which the mailq plugin needs to parse exim age literals), and the new `url.split_basic_auth(url)` helper that lets monitoring plugins accept HTTP basic auth via the URL itself instead of through separate `--username` / `--password` arguments (used by `php-fpm-status` and now `haproxy-status`)
7577
* Windows MSI still installs all plugins to ProgramFiles64Folder/ICINGA2/sbin/linuxfabrik, but does not depend on an Icinga2 agent any longer
7678

7779

check-plugins/haproxy-status/README.md

Lines changed: 37 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -56,49 +56,51 @@ Monitors HAProxy performance and health via the stats endpoint. Reports frontend
5656
5757
```text
5858
usage: haproxy-status [-h] [-V] [--always-ok] [-c CRIT] [--ignore IGNORE]
59-
[--insecure] [--lengthy] [--no-proxy] [-p PASSWORD]
60-
[--test TEST] [--timeout TIMEOUT] [-u URL]
61-
[--username USERNAME] [-w WARN]
59+
[--insecure] [--lengthy] [--no-proxy] [--test TEST]
60+
[--timeout TIMEOUT] [-u URL] [-w WARN]
6261
6362
Monitors HAProxy performance and health via the stats endpoint. Reports
6463
frontend and backend session usage, request rates, response times, error
6564
rates, and server states. Alerts when session usage exceeds the configured
6665
thresholds. Proxies, frontends, backends or individual servers can be filtered
6766
out with --ignore (e.g. an on-demand certbot backend that is only UP during
68-
cert renewal). Supports extended reporting via --lengthy.
67+
cert renewal). HTTP basic auth is supplied through the URL itself
68+
(`https://user:secret@host/server-status`); the credentials are stripped from
69+
the URL before the request is sent and instead carried in an `Authorization:
70+
Basic` header so they never reach the request line or any proxy access log.
71+
Supports extended reporting via --lengthy.
6972
7073
options:
71-
-h, --help show this help message and exit
72-
-V, --version show program's version number and exit
73-
--always-ok Always returns OK.
74-
-c, --critical CRIT CRIT threshold in percent. Default: >= 95
75-
--ignore IGNORE Ignore proxies, frontends, backends or servers
76-
matching this Python regular expression on the
77-
combined `<proxy>/<svname>` identifier (where `svname`
78-
is `FRONTEND`, `BACKEND` or an individual server
79-
name). Case-sensitive by default; use `(?i)` for case-
80-
insensitive matching. Can be specified multiple times.
81-
Example: `--ignore="^certbot/"` to ignore everything
82-
under the certbot proxy. Example:
83-
`--ignore="(?i)^certbot/"` for a case-insensitive
84-
match. Default: None
85-
--insecure This option explicitly allows insecure SSL
86-
connections.
87-
--lengthy Extended reporting.
88-
--no-proxy Do not use a proxy.
89-
-p, --password PASSWORD
90-
HAProxy stats auth password. Not needed for socket
91-
access.
92-
--test TEST For unit tests. Needs "path-to-stdout-file,path-to-
93-
stderr-file,expected-retc".
94-
--timeout TIMEOUT Network timeout in seconds. Default: 3 (seconds)
95-
-u, --url URL HAProxy stats URI. Accepts
96-
`unix:///path/to/haproxy.sock` or an HTTP(S) URL.
97-
Example: `--url https://webserver:8443/server-status`.
98-
Default: unix:///run/haproxy.sock
99-
--username USERNAME HAProxy stats auth username. Not needed for socket
100-
access. Default: haproxy-stats
101-
-w, --warning WARN WARN threshold in percent. Default: >= 80
74+
-h, --help show this help message and exit
75+
-V, --version show program's version number and exit
76+
--always-ok Always returns OK.
77+
-c, --critical CRIT CRIT threshold in percent. Default: >= 95
78+
--ignore IGNORE Ignore proxies, frontends, backends or servers matching
79+
this Python regular expression on the combined
80+
`<proxy>/<svname>` identifier (where `svname` is
81+
`FRONTEND`, `BACKEND` or an individual server name).
82+
Case-sensitive by default; use `(?i)` for case-
83+
insensitive matching. Can be specified multiple times.
84+
Example: `--ignore="^certbot/"` to ignore everything
85+
under the certbot proxy. Example:
86+
`--ignore="(?i)^certbot/"` for a case-insensitive
87+
match. Default: None
88+
--insecure This option explicitly allows insecure SSL connections.
89+
--lengthy Extended reporting.
90+
--no-proxy Do not use a proxy.
91+
--test TEST For unit tests. Needs "path-to-stdout-file,path-to-
92+
stderr-file,expected-retc".
93+
--timeout TIMEOUT Network timeout in seconds. Default: 3 (seconds)
94+
-u, --url URL HAProxy stats URI. Accepts
95+
`unix:///path/to/haproxy.sock` or an HTTP(S) URL. For
96+
HTTP basic auth, embed the credentials in the URL
97+
itself; the plugin strips them from the netloc before
98+
the request is sent and carries them in an
99+
`Authorization: Basic` header. Example: `--url
100+
https://webserver:8443/server-status`. Example: `--url
101+
https://stats:s3cret@webserver:8443/server-status`.
102+
Default: unix:///run/haproxy.sock
103+
-w, --warning WARN WARN threshold in percent. Default: >= 80
102104
```
103105

104106

check-plugins/haproxy-status/haproxy-status

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
"""See the check's README for more details."""
1212

1313
import argparse
14-
import base64
1514
import re
1615
import sys
1716

@@ -25,21 +24,24 @@ import lib.url
2524
from lib.globals import STATE_OK, STATE_UNKNOWN, STATE_WARN
2625

2726
__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
28-
__version__ = '2026041403'
27+
__version__ = '2026041407'
2928

3029
DESCRIPTION = """Monitors HAProxy performance and health via the stats endpoint. Reports
3130
frontend and backend session usage, request rates, response times, error rates, and server
3231
states. Alerts when session usage exceeds the configured thresholds. Proxies, frontends,
3332
backends or individual servers can be filtered out with --ignore (e.g. an on-demand certbot
34-
backend that is only UP during cert renewal). Supports extended reporting via --lengthy."""
33+
backend that is only UP during cert renewal). HTTP basic auth is supplied through the URL
34+
itself (`https://user:secret@host/server-status`); the credentials are stripped from the
35+
URL before the request is sent and instead carried in an `Authorization: Basic` header so
36+
they never reach the request line or any proxy access log. Supports extended reporting
37+
via --lengthy."""
3538

3639
DEFAULT_CRIT = 95 # %
3740
DEFAULT_INSECURE = False
3841
DEFAULT_LENGTHY = False
3942
DEFAULT_NO_PROXY = False
4043
DEFAULT_TIMEOUT = 3
4144
DEFAULT_URL = 'unix:///run/haproxy.sock'
42-
DEFAULT_USERNAME = 'haproxy-stats'
4345
DEFAULT_WARN = 80 # %
4446

4547
GOOD_STATUSES = {'OPEN', 'UP', 'no check'}
@@ -113,10 +115,17 @@ def parse_args():
113115
default=DEFAULT_NO_PROXY,
114116
)
115117

118+
# `--password` and `--username` were removed in favour of HTTP basic
119+
# auth embedded in the URL itself (e.g. `https://user:secret@host/`).
120+
# The flags stay registered (with argparse.SUPPRESS so they do not
121+
# show up in --help) so we can detect legacy usage in main() and
122+
# exit UNKNOWN with a clear migration pointer; otherwise
123+
# parser.parse_known_args() would silently swallow them and the
124+
# plugin would run without credentials, surfacing as a 401.
116125
parser.add_argument(
117126
'-p',
118127
'--password',
119-
help='HAProxy stats auth password. Not needed for socket access.',
128+
help=argparse.SUPPRESS,
120129
dest='PASSWORD',
121130
)
122131

@@ -140,19 +149,21 @@ def parse_args():
140149
'--url',
141150
help='HAProxy stats URI. '
142151
'Accepts `unix:///path/to/haproxy.sock` or an HTTP(S) URL. '
152+
'For HTTP basic auth, embed the credentials in the URL itself; '
153+
'the plugin strips them from the netloc before the request is '
154+
'sent and carries them in an `Authorization: Basic` header. '
143155
'Example: `--url https://webserver:8443/server-status`. '
156+
'Example: `--url https://stats:s3cret@webserver:8443/server-status`. '
144157
'Default: %(default)s',
145158
dest='URL',
146159
default=DEFAULT_URL,
147160
)
148161

162+
# See the comment on --password above.
149163
parser.add_argument(
150164
'--username',
151-
help='HAProxy stats auth username. '
152-
'Not needed for socket access. '
153-
'Default: %(default)s',
165+
help=argparse.SUPPRESS,
154166
dest='USERNAME',
155-
default=DEFAULT_USERNAME,
156167
)
157168

158169
parser.add_argument(
@@ -177,6 +188,21 @@ def main():
177188
except SystemExit:
178189
sys.exit(STATE_UNKNOWN)
179190

191+
# `--username` and `--password` were removed in favour of HTTP
192+
# basic auth embedded in the URL itself. They are still
193+
# registered (with argparse.SUPPRESS) so we can hard-UNKNOWN
194+
# here with a clear migration pointer instead of letting
195+
# parse_known_args silently swallow them and surface a 401 from
196+
# the haproxy stats endpoint.
197+
if args.USERNAME is not None or args.PASSWORD is not None:
198+
lib.base.cu(
199+
'--username and --password were removed. Embed the HTTP '
200+
'basic auth credentials in the URL itself, e.g. '
201+
'`--url=https://user:secret@host:8443/server-status`. The '
202+
'plugin strips them from the netloc before the request is '
203+
'sent and carries them in an Authorization: Basic header.'
204+
)
205+
180206
if args.IGNORE is None:
181207
args.IGNORE = []
182208

@@ -199,15 +225,17 @@ def main():
199225
)
200226

201227
if url.startswith('http'):
202-
url += ';csv'
203-
headers = {}
204-
if args.USERNAME and args.PASSWORD:
205-
auth = f'{args.USERNAME}:{args.PASSWORD}'
206-
encoded_auth = lib.txt.to_text(base64.b64encode(lib.txt.to_bytes(auth)))
207-
headers = {'Authorization': f'Basic {encoded_auth}'}
228+
# Move any `user:secret@` userinfo from the URL into an
229+
# Authorization: Basic header so the credentials never reach
230+
# the request line or any proxy access log. Append the
231+
# `;csv` query that the haproxy stats endpoint expects to
232+
# the stripped URL afterwards so the userinfo split stays
233+
# confined to lib.url.split_basic_auth().
234+
stripped_url, headers = lib.url.split_basic_auth(url)
235+
stripped_url += ';csv'
208236
result = lib.base.coe(
209237
lib.url.fetch(
210-
url,
238+
stripped_url,
211239
header=headers,
212240
insecure=args.INSECURE,
213241
no_proxy=args.NO_PROXY,

check-plugins/haproxy-status/unit-test/run

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,32 @@ TESTS = [
144144
'assert-retc': STATE_UNKNOWN,
145145
'assert-in': ['Invalid regular expression'],
146146
},
147+
148+
# --username and --password were removed in favour of HTTP basic
149+
# auth embedded in the URL itself. The flags stay registered with
150+
# argparse.SUPPRESS so we can hard-UNKNOWN here with a clear
151+
# migration pointer instead of letting parse_known_args silently
152+
# swallow them and surface a 401 from the haproxy stats endpoint.
153+
{
154+
'id': 'unknown-legacy-username',
155+
'test': 'stdout/all-backends-up,,0',
156+
'params': '--username=stats',
157+
'assert-retc': STATE_UNKNOWN,
158+
'assert-in': [
159+
'--username and --password were removed.',
160+
'https://user:secret@host',
161+
],
162+
},
163+
{
164+
'id': 'unknown-legacy-password',
165+
'test': 'stdout/all-backends-up,,0',
166+
'params': '--password=secret',
167+
'assert-retc': STATE_UNKNOWN,
168+
'assert-in': [
169+
'--username and --password were removed.',
170+
'https://user:secret@host',
171+
],
172+
},
147173
]
148174

149175

requirements.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,9 +273,9 @@ keystoneauth1==5.11.1 \
273273
# via
274274
# python-keystoneclient
275275
# python-novaclient
276-
linuxfabrik-lib==3.1.1 \
277-
--hash=sha256:b37049393fa5c21114e8dc001b00d4ec063b4c96564fb3c02f98dd474b0b2831 \
278-
--hash=sha256:b935629e6090b6d258d6ac47ed5144127f2380a19d1d518cfb799e727b7c4c90
276+
linuxfabrik-lib==3.2.0 \
277+
--hash=sha256:3e828c866cfd244f72a208d57dacab6af697c4e7f249a3ef65dd4fe3e0298215 \
278+
--hash=sha256:8597164cf5ba08cfac9e6c92d4de525c8a1d3f9b5605e071b07d8da434726253
279279
# via -r requirements.in
280280
lxml==6.0.4 \
281281
--hash=sha256:0132bb040e9bb5a199302e12bf942741defbc52922a2a06ce9ff7be0d0046483 \

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ deps =
1616
# that use them only work through the sys.path.insert(0, '..')
1717
# shadow and break on any CI runner that does not have the lib
1818
# repo checked out side-by-side.
19-
linuxfabrik-lib>=3.1.1
19+
linuxfabrik-lib>=3.2.0
2020
lxml
2121
psutil
2222
python-keystoneclient

0 commit comments

Comments
 (0)