Files
crowdsec-bouncer-traefik-pl…/tests/e2e/mock/scenarios/tls-system-ca/run.sh
T
f4dcd933c8 🐛 fall back to system trust store when no custom TLS CA is set (#331)
* 🐛 fall back to system trust store when no custom TLS CA is set

Closes #327.

Until now, configuring `crowdsecLapiScheme=https` forced the operator
to either provide `crowdsecLapiTLSCertificateAuthority` (a custom CA)
or set `crowdsecLapiTLSInsecureVerify=true` — there was no way to rely
on the host's system trust store, which is the expected setup when the
LAPI sits behind a reverse proxy with a publicly trusted (e.g. Let's
Encrypt) certificate.

Two contributing bugs:
  - `validateParamsTLS` rejected an empty CA up-front.
  - `getTLSConfig` always allocated an empty `tls.Config.RootCAs`,
    which silently disabled the standard library's fall-back to
    `x509.SystemCertPool()`.

Fix: drop the validation error for the empty-CA case and only allocate
`RootCAs` when a custom CA is actually provided. Same change applies
symmetrically to the AppSec path since the helper is shared.

Add unit tests covering the four meaningful states (HTTP, HTTPS with
system CA, HTTPS with custom CA, HTTPS with insecure verify) plus the
malformed-PEM rejection. README updated to document the system trust
store as an explicit option for both LAPI and AppSec HTTPS.

* 📝 fix gofmt alignment in TLS test struct

*  update existing test: https without CA is now accepted

* ♻️ tests: hoist shared validPEM to package level, rename cfgGarbage

Address review on #331:
- the self-signed validPEM block was duplicated in two test funcs; declare it
  once at package level and drop both local copies.
- rename cfgGarbage -> cfgInvalidCA (and its test case) for a descriptive name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  e2e mock: add tls-system-ca scenario (HTTPS LAPI via system trust store)

CI regression coverage for this PR: with no custom CA configured, the bouncer
must fall back to the OS/system trust store for an HTTPS LAPI.

In the binary suite the "system trust store" is whatever Go's
x509.SystemCertPool() reads, which honours SSL_CERT_FILE on the Traefik process.
The scenario mints a throwaway CA, serves the mock LAPI over HTTPS with a cert
signed by it, and runs the stack twice:
  - positive: SSL_CERT_FILE = our CA       -> LAPI trusted    -> 200
  - negative: SSL_CERT_FILE = empty bundle  -> not trusted     -> 403 (fail-closed)
The negative run proves the patch still VERIFIES (not an insecure skip).

- mocklapi: optional --lapi-tls-cert/--lapi-tls-key to serve the LAPI over TLS.
- common.sh: opt-in LAPI_TLS_CERT/KEY (HTTPS mock) and TRAEFIK_SSL_CERT_FILE
  (inject SSL_CERT_FILE into Traefik); both default-empty, other scenarios
  unaffected.
- adds openssl as a scenario-only dependency.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 09:27:32 +02:00

67 lines
2.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# Scenario: HTTPS LAPI with no custom CA configured -> the bouncer must fall back
# to the OS/system trust store (PR #331). In the binary suite the "system trust
# store" is whatever Go's x509.SystemCertPool() reads, which honours SSL_CERT_FILE
# on the Traefik process. We mint a throwaway CA, serve the mock LAPI over HTTPS
# with a cert signed by it, and run the stack twice:
#
# positive: SSL_CERT_FILE = our CA -> LAPI trusted -> 200
# negative: SSL_CERT_FILE = empty bundle -> LAPI not trusted -> 403
#
# live mode is fail-closed, so a TLS error becomes a 403. The negative run proves
# the patch still VERIFIES (it is not an insecure skip).
#
# Extra dependency vs other scenarios: openssl.
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=../../lib/common.sh
source "$HERE/../../lib/common.sh"
SCENARIO=tls-system-ca
SCENARIO_NAME="$SCENARIO"
SCENARIO_LOG="/tmp/e2e-mock-${SCENARIO}.log"
CERT_DIR="$(mktemp -d)"
cleanup() {
local rc=$?
if (( rc != 0 )); then
dump_diagnostics > "$SCENARIO_LOG" 2>&1 || true
echo "[$SCENARIO] failed. Logs written to $SCENARIO_LOG" >&2
fi
stop_stack
rm -rf "$CERT_DIR"
exit $rc
}
trap cleanup EXIT
echo "[$SCENARIO] minting throwaway CA + LAPI cert (SAN=IP:127.0.0.1)..."
openssl ecparam -name prime256v1 -genkey -noout -out "$CERT_DIR/ca.key" 2>/dev/null
openssl req -x509 -new -key "$CERT_DIR/ca.key" -sha256 -days 3650 \
-subj "/CN=crowdsec-bouncer e2e test CA" -out "$CERT_DIR/ca.crt" 2>/dev/null
openssl ecparam -name prime256v1 -genkey -noout -out "$CERT_DIR/lapi.key" 2>/dev/null
openssl req -new -key "$CERT_DIR/lapi.key" -subj "/CN=lapi" -out "$CERT_DIR/lapi.csr" 2>/dev/null
openssl x509 -req -in "$CERT_DIR/lapi.csr" -CA "$CERT_DIR/ca.crt" -CAkey "$CERT_DIR/ca.key" \
-CAcreateserial -days 3650 -sha256 -out "$CERT_DIR/lapi.crt" \
-extfile <(printf "subjectAltName=IP:127.0.0.1\nbasicConstraints=CA:FALSE\nkeyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth") 2>/dev/null
: > "$CERT_DIR/empty.crt" # an empty bundle = a system store that trusts nothing
# The mock serves the same CA-signed cert in both runs; only Traefik's trust differs.
export LAPI_TLS_CERT="$CERT_DIR/lapi.crt" LAPI_TLS_KEY="$CERT_DIR/lapi.key"
echo "[$SCENARIO] === positive: CA in the system trust store ==="
export TRAEFIK_SSL_CERT_FILE="$CERT_DIR/ca.crt"
start_stack "$HERE"
echo "[$SCENARIO] HTTPS LAPI verifies via system trust store -> request passes (200)"
assert_status "http://127.0.0.1:${WEB_PORT}/foo" 200 -H "X-Forwarded-For: 1.2.3.4"
stop_stack
echo "[$SCENARIO] === negative: CA absent from the system trust store ==="
export TRAEFIK_SSL_CERT_FILE="$CERT_DIR/empty.crt"
start_stack "$HERE"
echo "[$SCENARIO] LAPI cert not trusted -> TLS fails, fail-closed (403)"
assert_status "http://127.0.0.1:${WEB_PORT}/foo" 403 -H "X-Forwarded-For: 1.2.3.4"
stop_stack
echo "[$SCENARIO] OK"