feat: add mTLS client certificate authentication to REST generator#1681
Conversation
|
DCO Assistant Lite bot All contributors have signed the DCO ✍️ ✅ |
|
I have read the DCO Document and I hereby sign the DCO |
jmartin-tech
left a comment
There was a problem hiding this comment.
This look really useful, I have requested a few tweaks based on static code review.
It may take a bit to configure a testing env for final acceptance. If you happen to have details to share for setup of a container or even a light testing application that and the configuration example for garak that could be used to validate this that would be appreciated.
| # load passphrase from env var if specified | ||
| self.client_key_passphrase = None | ||
| if self.client_key_passphrase_env_var is not None: | ||
| self.client_key_passphrase = os.getenv(self.client_key_passphrase_env_var) | ||
| if self.client_key_passphrase is None: | ||
| raise BadGeneratorException( | ||
| f"client_key_passphrase_env_var '{self.client_key_passphrase_env_var}' " | ||
| "is set but the environment variable is not defined" | ||
| ) |
There was a problem hiding this comment.
Prefer items pulled from env vars override _validate_env_var() and call the super() to supply default support:
def _validate_env_var(self):
super()._validate_env_var()
if self.client_key_passphrase is None and self.client_key_passphrase_env_var is not None:
self.client_key_passphrase = os.getenv(self.client_key_passphrase_env_var, default=None)
if self.client_key_passphrase is None:
raise BadGeneratorException(
f"client_key_passphrase_env_var '{self.client_key_passphrase_env_var}' "
"is set but the environment variable is not defined"
)Note that _validate_env_var() is called as the last stop of the earlier call to self._load_config(config_root) moving this validation to an earlier stage in this object's intialization.
|
Pushed the fixes addressing all four review items:
New in this revision:
Testing scaffolding for manual validation: All 32 tests pass: |
Add mutual TLS (mTLS) client certificate support to RestGenerator, enabling authentication with endpoints that require client certificates. New configuration options: - client_cert: path to PEM-encoded client certificate - client_key: path to client private key (optional if cert contains key) - client_key_passphrase_env_var: env var name holding encrypted key passphrase Implementation details: - Custom _MtlsAdapter (HTTPAdapter subclass) injects pre-configured SSLContext into urllib3 connection pools for both direct and proxied connections - Session-based request dispatch when mTLS is active; falls back to stateless requests.method() calls otherwise - Passphrase read from environment variable (never from config files) and cleared from memory immediately after loading into SSLContext - _unsafe_attributes prevents passphrase and session from leaking into pickled/serialized state via Configurable.__getstate__() Security hardening: - HTTPS URI validation: rejects http:// URIs when client_cert is set to prevent silent security downgrade - Session cleanup via __del__ to close connection pools Tests: - 9 new mTLS unit tests covering construction, validation, error paths, passphrase loading, CA bundle wiring, _call_model session dispatch, and http:// URI rejection - All 21 tests pass (12 existing + 9 new) Documentation: - Updated garak.generators.rest.rst with mTLS configuration reference, usage examples for cert+key, combined PEM, and encrypted key scenarios Signed-off-by: JakeBx <jacob.j.lee@live.com>
47d9e6b to
3adcbf7
Compare
- Move passphrase env var loading to _validate_env_var() with super() call - Extract mTLS session creation to _load_unsafe() for multiprocessing support - Trim _unsafe_attributes to only ['_mtls_session'] - Update comment to reference _load_unsafe() - Add pickle roundtrip unit test with real certs - Add self-contained mTLS integration test suite (10 tests) Signed-off-by: JakeBx <jacob.j.lee@live.com>
Signed-off-by: JakeBx <jacob.j.lee@live.com>
Python 3.13 OpenSSL enforces RFC 5280 strictly and rejects certificates missing the Authority Key Identifier extension. Add SubjectKeyIdentifier and AuthorityKeyIdentifier to all generated certs in the mtls_certs fixture (CA, server, client, ECDSA client) to fix 6 failing integration tests on Python 3.13. Signed-off-by: JakeBx <jacob.j.lee@live.com>
3adcbf7 to
680509f
Compare
jmartin-tech
left a comment
There was a problem hiding this comment.
Testing looks good, thanks for a great capability add!
proxy-1 | 172.19.0.1 - - [29/Apr/2026:20:59:28 +0000] "POST /openrouter HTTP/1.1" 200 903 "-" "garak/0.14.2.pre1 (LLM vulnerability scanner https://garak.ai)" ssl_client_s_dn="CN=garak-client,OU=Garak,O=JakeBx,L=Sydney,ST=NSW,C=AU" ssl_client_verify="SUCCESS"
proxy-1 | 172.19.0.1 - - [29/Apr/2026:20:59:30 +0000] "POST /openrouter HTTP/1.1" 200 1265 "-" "garak/0.14.2.pre1 (LLM vulnerability scanner https://garak.ai)" ssl_client_s_dn="CN=garak-client,OU=Garak,O=JakeBx,L=Sydney,ST=NSW,C=AU" ssl_client_verify="SUCCESS"
proxy-1 | 172.19.0.1 - - [29/Apr/2026:20:59:32 +0000] "POST /openrouter HTTP/1.1" 200 901 "-" "garak/0.14.2.pre1 (LLM vulnerability scanner https://garak.ai)" ssl_client_s_dn="CN=garak-client,OU=Garak,O=JakeBx,L=Sydney,ST=NSW,C=AU" ssl_client_verify="SUCCESS"
proxy-1 | 172.19.0.1 - - [29/Apr/2026:20:59:34 +0000] "POST /openrouter HTTP/1.1" 200 1133 "-" "garak/0.14.2.pre1 (LLM vulnerability scanner https://garak.ai)" ssl_client_s_dn="CN=garak-client,OU=Garak,O=JakeBx,L=Sydney,ST=NSW,C=AU" ssl_client_verify="SUCCESS"
proxy-1 | 172.19.0.1 - - [29/Apr/2026:21:02:28 +0000] "POST /openrouter HTTP/1.1" 200 1305 "-" "garak/0.14.2.pre1 (LLM vulnerability scanner https://garak.ai)" ssl_client_s_dn="CN=garak-client-encrypted,OU=Garak,O=JakeBx,L=Sydney,ST=NSW,C=AU" ssl_client_verify="SUCCESS"
proxy-1 | 172.19.0.1 - - [29/Apr/2026:21:02:31 +0000] "POST /openrouter HTTP/1.1" 200 2098 "-" "garak/0.14.2.pre1 (LLM vulnerability scanner https://garak.ai)" ssl_client_s_dn="CN=garak-client-encrypted,OU=Garak,O=JakeBx,L=Sydney,ST=NSW,C=AU" ssl_client_verify="SUCCESS"
proxy-1 | 172.19.0.1 - - [29/Apr/2026:21:02:35 +0000] "POST /openrouter HTTP/1.1" 200 2368 "-" "garak/0.14.2.pre1 (LLM vulnerability scanner https://garak.ai)" ssl_client_s_dn="CN=garak-client-encrypted,OU=Garak,O=JakeBx,L=Sydney,ST=NSW,C=AU" ssl_client_verify="SUCCESS"
Add mutual TLS (mTLS) client certificate support to RestGenerator, enabling authentication with endpoints that require client certificates.
mTLS is a hard requirement in regulated industries (financial services, healthcare, and government environments) where endpoints mandate mutual authentication as part of their security posture. Without this, garak cannot be used as part of a compliant LLM security testing pipeline against these targets.
Major providers are already shipping mTLS support: OpenAI's Mutual TLS Beta Program enables client certificate authentication on /v1/chat/completions today. Without this change, garak users in regulated environments cannot run scans against these endpoints.
New configuration options:
Implementation details:
Security hardening:
__del__to close connection poolsTests:
Documentation:
Verification
Configuration
{ "rest": { "RestGenerator": { "uri": "https://your-mtls-endpoint.example.com/v1/generate", "client_cert": "/path/to/client.pem", "client_key": "/path/to/client.key", "client_key_passphrase_env_var": "GARAK_CLIENT_KEY_PASS" } } }For a combined cert+key PEM (no separate key file):
{ "rest": { "RestGenerator": { "uri": "https://your-mtls-endpoint.example.com/v1/generate", "client_cert": "/path/to/combined.pem" } } }CLI invocation
Tests
All cases pass.
test_rest_mtls_no_cert_direct_pathverifykwarg still passedtest_rest_mtls_both_cert_and_keytest_rest_mtls_cert_onlytest_rest_mtls_key_without_cert_raisesBadGeneratorExceptiontest_rest_mtls_nonexistent_cert_raisesBadGeneratorExceptiontest_rest_mtls_passphrase_loads_from_envtest_rest_mtls_verify_ssl_ca_pathtest_rest_mtls_call_model_uses_session_call_modelroutes through_mtls_session.requesttest_rest_mtls_http_uri_raiseshttp://URI with mTLS →BadGeneratorExceptionhttp://) all raiseBadGeneratorExceptionwith clear messagesdocs/source/garak.generators.rest.rstHappy to iterate on the approach if the implementation direction needs adjusting