Download raw body.
Relayd client ca support: optionality and filtering
Hi,
I'm following up on this message to see whether there would be any
interest in merging this. This adds significant functionality to
relayd with minimal changes to the codebase and has been running
cleanly on my servers for some time. Patch still applies cleanly to
7.7 and current.
CC'ing some potentially interested developers.
Best,
-- PO
On Thu, Oct 31, 2024 at 5:35 PM Persistent Oscillator
<persistentoscillator@gmail.com> wrote:
>
> Hi,
>
> Following up on the recent commit adding client ca verification support
> to relayd, I wanted to ask whether there would be interest in the
> following additional patch. It does two things:
>
> - It allows client ca verification to be either mandatory or optional,
> integrating code submitted to this list by Markus Läll in December
> 2021 [1].
> - It adds a "client ca" test verifying whether a client certificate was
> indeed provided, integrating the test into the usual relayd filtering
> rules. This expands the use case of client certificates, allowing for
> more fine-grained checks. In addition to a straightforward check
> whether a client CA was provided by the client and accepted by relayd,
> it can verify the hash, the subject, the issuer or the name of the
> client certificate.
>
> The test allows, for example, to:
>
> pass request quick client ca header "Host" value "secure.example.com"
> block request header "Host" value "secure.example.com"
>
> The filter could be more nuanced (eg based on the hash of the specific
> client certificate when relayd accepts multiple client certificates).
> One could also forward clients who provided a client cert to a different
> backend, etc.
>
> Kind regards,
>
> -- PO
>
> [1]: https://www.mail-archive.com/tech@openbsd.org/msg67360.html
>
> diff --git a/regress/usr.sbin/relayd/Relayd.pm b/regress/usr.sbin/relayd/Relayd.pm
> index 2a6aa926d16..6c8ddad08a9 100644
> --- a/regress/usr.sbin/relayd/Relayd.pm
> +++ b/regress/usr.sbin/relayd/Relayd.pm
> @@ -88,6 +88,9 @@ sub new {
> if ($self->{verifyclient}) {
> print $fh "\n\ttls client ca client-ca.crt";
> }
> + if ($self->{verifyclientoptional}) {
> + print $fh "\n\ttls client ca client-ca.crt optional";
> + }
> # substitute variables in config file
> foreach (@protocol) {
> s/(\$[a-z]+)/$1/eeg;
> diff --git a/regress/usr.sbin/relayd/args-https-filter-clientca-fail.pl b/regress/usr.sbin/relayd/args-https-filter-clientca-fail.pl
> new file mode 100644
> index 00000000000..cef405e53a1
> --- /dev/null
> +++ b/regress/usr.sbin/relayd/args-https-filter-clientca-fail.pl
> @@ -0,0 +1,35 @@
> +# test clientca filter over http relay
> +
> +use strict;
> +use warnings;
> +
> +our %args = (
> + client => {
> + ssl => 1,
> + func => \&http_client,
> + httpnok => 1,
> + nocheck => 1,
> + loggrep => qr/HTTP\/1\.0 403 Forbidden/,
> + },
> + relayd => {
> + forwardssl => 1,
> + listenssl => 1,
> + verifyclientoptional => 1,
> + protocol => [ "http",
> + "return error",
> + "match request tag NOCLIENTCA",
> + "pass request quick client ca tag HASCLIENTCA",
> + "block",
> + ],
> + loggrep => qr/NOCLIENTCA.*403 Forbidden/,
> + },
> + server => {
> + func => \&http_server,
> + ssl => 1,
> + noserver => 1,
> + nocheck => 1,
> + },
> + LEN => 427,
> +);
> +
> +1;
> diff --git a/regress/usr.sbin/relayd/args-https-filter-clientca-hash.pl b/regress/usr.sbin/relayd/args-https-filter-clientca-hash.pl
> new file mode 100644
> index 00000000000..86263f075f3
> --- /dev/null
> +++ b/regress/usr.sbin/relayd/args-https-filter-clientca-hash.pl
> @@ -0,0 +1,36 @@
> +# test clientca hash filter over http relay
> +
> +use strict;
> +use warnings;
> +
> +my $sha = qx(openssl x509 -outform der -in client.crt|sha256|tr -d '\n');
> +
> +our %args = (
> + client => {
> + ssl => 1,
> + offertlscert => 1,
> + func => \&http_client,
> + loggrep => 'Issuer.*/OU=relayd/',
> + },
> + relayd => {
> + forwardssl => 1,
> + listenssl => 1,
> + verifyclientoptional => 1,
> + protocol => [ "http",
> + "return error",
> + "match request tag NOCLIENTCA",
> + "pass request quick client ca clhash \"SHA256:$sha\" \\
> + tag HASCLIENTCAHASH",
> + "block",
> + ],
> + loggrep => qr/HASCLIENTCAHASH.*GET/,
> + },
> + server => {
> + func => \&http_server,
> + ssl => 1,
> + },
> + LEN => 251,
> + md5 => "bc3a3f39af35fe5b1687903da2b00c7f",
> +);
> +
> +1;
> diff --git a/regress/usr.sbin/relayd/args-https-filter-clientca-issuer.pl b/regress/usr.sbin/relayd/args-https-filter-clientca-issuer.pl
> new file mode 100644
> index 00000000000..029b03adf16
> --- /dev/null
> +++ b/regress/usr.sbin/relayd/args-https-filter-clientca-issuer.pl
> @@ -0,0 +1,35 @@
> +# test clientca hash filter over http relay
> +
> +use strict;
> +use warnings;
> +
> +our %args = (
> + client => {
> + ssl => 1,
> + offertlscert => 1,
> + func => \&http_client,
> + loggrep => 'Issuer.*/OU=relayd/',
> + },
> + relayd => {
> + forwardssl => 1,
> + listenssl => 1,
> + verifyclientoptional => 1,
> + protocol => [ "http",
> + "return error",
> + "match request tag NOCLIENTCA",
> + "pass request quick client ca issuer \\
> + \"\/L=OpenBSD\/O=relayd-regress\/OU=client-ca\/CN=root\"\\
> + tag HASCLIENTCAISSUER",
> + "block",
> + ],
> + loggrep => qr/HASCLIENTCAISSUER.*GET/,
> + },
> + server => {
> + func => \&http_server,
> + ssl => 1,
> + },
> + LEN => 251,
> + md5 => "bc3a3f39af35fe5b1687903da2b00c7f",
> +);
> +
> +1;
> diff --git a/regress/usr.sbin/relayd/args-https-filter-clientca-name.pl b/regress/usr.sbin/relayd/args-https-filter-clientca-name.pl
> new file mode 100644
> index 00000000000..83eff0bcd77
> --- /dev/null
> +++ b/regress/usr.sbin/relayd/args-https-filter-clientca-name.pl
> @@ -0,0 +1,34 @@
> +# test clientca hash filter over http relay
> +
> +use strict;
> +use warnings;
> +
> +our %args = (
> + client => {
> + ssl => 1,
> + offertlscert => 1,
> + func => \&http_client,
> + loggrep => 'Issuer.*/OU=relayd/',
> + },
> + relayd => {
> + forwardssl => 1,
> + listenssl => 1,
> + verifyclientoptional => 1,
> + protocol => [ "http",
> + "return error",
> + "match request tag NOCLIENTCA",
> + "pass request quick client ca name \"localhost\" \\
> + tag HASCLIENTCANAME",
> + "block",
> + ],
> + loggrep => qr/HASCLIENTCANAME.*GET/,
> + },
> + server => {
> + func => \&http_server,
> + ssl => 1,
> + },
> + LEN => 251,
> + md5 => "bc3a3f39af35fe5b1687903da2b00c7f",
> +);
> +
> +1;
> diff --git a/regress/usr.sbin/relayd/args-https-filter-clientca-subj.pl b/regress/usr.sbin/relayd/args-https-filter-clientca-subj.pl
> new file mode 100644
> index 00000000000..7fbbfabbd6c
> --- /dev/null
> +++ b/regress/usr.sbin/relayd/args-https-filter-clientca-subj.pl
> @@ -0,0 +1,35 @@
> +# test clientca hash filter over http relay
> +
> +use strict;
> +use warnings;
> +
> +our %args = (
> + client => {
> + ssl => 1,
> + offertlscert => 1,
> + func => \&http_client,
> + loggrep => 'Issuer.*/OU=relayd/',
> + },
> + relayd => {
> + forwardssl => 1,
> + listenssl => 1,
> + verifyclientoptional => 1,
> + protocol => [ "http",
> + "return error",
> + "match request tag NOCLIENTCA",
> + "pass request quick client ca subject \\
> + \"\/L=OpenBSD\/O=relayd-regress\/OU=client\/CN=localhost\" \\
> + tag HASCLIENTCASUBJ",
> + "block",
> + ],
> + loggrep => qr/HASCLIENTCASUBJ.*GET/,
> + },
> + server => {
> + func => \&http_server,
> + ssl => 1,
> + },
> + LEN => 251,
> + md5 => "bc3a3f39af35fe5b1687903da2b00c7f",
> +);
> +
> +1;
> diff --git a/regress/usr.sbin/relayd/args-https-filter-clientca.pl b/regress/usr.sbin/relayd/args-https-filter-clientca.pl
> new file mode 100644
> index 00000000000..3bc2ee7f201
> --- /dev/null
> +++ b/regress/usr.sbin/relayd/args-https-filter-clientca.pl
> @@ -0,0 +1,33 @@
> +# test clientca filter over http relay
> +
> +use strict;
> +use warnings;
> +
> +our %args = (
> + client => {
> + ssl => 1,
> + offertlscert => 1,
> + func => \&http_client,
> + loggrep => 'Issuer.*/OU=relayd/',
> + },
> + relayd => {
> + forwardssl => 1,
> + listenssl => 1,
> + verifyclientoptional => 1,
> + protocol => [ "http",
> + "return error",
> + "match request tag NOCLIENTCA",
> + "pass request quick client ca tag HASCLIENTCA",
> + "block",
> + ],
> + loggrep => qr/HASCLIENTCA.*GET/,
> + },
> + server => {
> + func => \&http_server,
> + ssl => 1,
> + },
> + LEN => 251,
> + md5 => "bc3a3f39af35fe5b1687903da2b00c7f",
> +);
> +
> +1;
> diff --git a/usr.sbin/relayd/parse.y b/usr.sbin/relayd/parse.y
> index fcdfb8e92e3..e8671cb0b63 100644
> --- a/usr.sbin/relayd/parse.y
> +++ b/usr.sbin/relayd/parse.y
> @@ -179,7 +179,7 @@ typedef struct {
> %token TIMEOUT TLS TO ROUTER RTLABEL TRANSPARENT URL WITH TTL RTABLE
> %token MATCH PARAMS RANDOM LEASTSTATES SRCHASH KEY CERTIFICATE PASSWORD ECDHE
> %token EDH TICKETS CONNECTION CONNECTIONS CONTEXT ERRORS STATE CHANGES CHECKS
> -%token WEBSOCKETS PFLOG CLIENT
> +%token WEBSOCKETS PFLOG CLIENT OPTIONAL
> %token <v.string> STRING
> %token <v.number> NUMBER
> %type <v.string> context hostname interface table value path
> @@ -188,6 +188,7 @@ typedef struct {
> %type <v.number> opttls opttlsclient
> %type <v.number> redirect_proto relay_proto match pflog
> %type <v.number> action ruleaf key_option
> +%type <v.number> clientcaopt
> %type <v.port> port
> %type <v.host> host
> %type <v.addr> address rulesrc ruledst addrprefix
> @@ -235,6 +236,10 @@ opttlsclient : /*empty*/ { $$ = 0; }
> | WITH TLS { $$ = 1; }
> ;
>
> +clientcaopt : /*empty*/ { $$ = 0; }
> + | OPTIONAL { $$ = 1; }
> + ;
> +
> http_type : HTTP { $$ = 0; }
> | STRING {
> if (strcmp("https", $1) == 0) {
> @@ -1351,7 +1356,7 @@ tlsflags : SESSION TICKETS { proto->tickets = 1; }
> name->name = $2;
> TAILQ_INSERT_TAIL(&proto->tlscerts, name, entry);
> }
> - | CLIENT CA STRING {
> + | CLIENT CA STRING clientcaopt {
> if (strlcpy(proto->tlsclientca, $3,
> sizeof(proto->tlsclientca)) >=
> sizeof(proto->tlsclientca)) {
> @@ -1359,6 +1364,9 @@ tlsflags : SESSION TICKETS { proto->tickets = 1; }
> free($3);
> YYERROR;
> }
> + if ($4) {
> + proto->tlsflags |= TLSFLAG_CLIENT_OPTIONAL;
> + }
> free($3);
> }
> | NO flag { proto->tlsflags &= ~($2); }
> @@ -1498,6 +1506,31 @@ ruleopts : METHOD STRING {
> rule->rule_method = id;
> free($2);
> }
> + | CLIENT CA STRING STRING {
> + keytype = KEY_TYPE_CLCACHK;
> + rule->rule_kv[keytype].kv_type = keytype;
> + if (strcasecmp("clhash", $3) == 0) {
> + rule->rule_kv[keytype].kv_key = strdup($3);
> + } else if (strcasecmp("issuer", $3) == 0) {
> + rule->rule_kv[keytype].kv_key = strdup($3);
> + } else if (strcasecmp("name", $3) == 0) {
> + rule->rule_kv[keytype].kv_key = strdup($3);
> + } else if (strcasecmp("subject", $3) == 0) {
> + rule->rule_kv[keytype].kv_key = strdup($3);
> + } else {
> + yyerror("invalid client ca key: %s", $3);
> + YYERROR;
> + }
> + rule->rule_kv[keytype].kv_value = strdup($4);
> + free($3);
> + free($4);
> + }
> + | CLIENT CA {
> + keytype = KEY_TYPE_CLCACHK;
> + rule->rule_kv[keytype].kv_type = keytype;
> + rule->rule_kv[keytype].kv_key = strdup("*");
> + rule->rule_kv[keytype].kv_value = strdup("*");
> + }
> | COOKIE key_option STRING value {
> keytype = KEY_TYPE_COOKIE;
> rule->rule_kv[keytype].kv_key = strdup($3);
> @@ -2468,6 +2501,7 @@ lookup(char *s)
> { "nodelay", NODELAY },
> { "nothing", NOTHING },
> { "on", ON },
> + { "optional", OPTIONAL },
> { "params", PARAMS },
> { "parent", PARENT },
> { "pass", PASS },
> diff --git a/usr.sbin/relayd/relay.c b/usr.sbin/relayd/relay.c
> index 6d0970802c5..9dc06969624 100644
> --- a/usr.sbin/relayd/relay.c
> +++ b/usr.sbin/relayd/relay.c
> @@ -179,6 +179,9 @@ relay_ruledebug(struct relay_rule *rule)
> continue;
>
> switch (kv->kv_type) {
> + case KEY_TYPE_CLCACHK:
> + fprintf(stderr, "client ca ");
> + break;
> case KEY_TYPE_COOKIE:
> fprintf(stderr, "cookie ");
> break;
> @@ -2266,7 +2269,10 @@ relay_tls_ctx_create(struct relay *rlay)
> }
> purge_key(&buf, len);
>
> - tls_config_verify_client(tls_cfg);
> + if (rlay->rl_proto->tlsflags & TLSFLAG_CLIENT_OPTIONAL)
> + tls_config_verify_client_optional(tls_cfg);
> + else
> + tls_config_verify_client(tls_cfg);
> }
> rlay->rl_tls_client_ca_fd = -1;
>
> diff --git a/usr.sbin/relayd/relay_http.c b/usr.sbin/relayd/relay_http.c
> index 0c511d0b9eb..051e33d1f83 100644
> --- a/usr.sbin/relayd/relay_http.c
> +++ b/usr.sbin/relayd/relay_http.c
> @@ -71,6 +71,8 @@ int relay_httpurl_test(struct ctl_relay_event *,
> struct relay_rule *, struct kvlist *);
> int relay_httpcookie_test(struct ctl_relay_event *,
> struct relay_rule *, struct kvlist *);
> +int relay_clientca_test(struct ctl_relay_event *,
> + struct relay_rule *, struct kvlist *);
> int relay_apply_actions(struct ctl_relay_event *, struct kvlist *,
> struct relay_table *);
> int relay_match_actions(struct ctl_relay_event *,
> @@ -1718,6 +1720,41 @@ relay_match_actions(struct ctl_relay_event *cre, struct relay_rule *rule,
> return (0);
> }
>
> +int
> +relay_clientca_test(struct ctl_relay_event *cre, struct relay_rule *rule,
> + struct kvlist *actions)
> +{
> + struct kv *kv = &rule->rule_kv[KEY_TYPE_CLCACHK];
> + struct kv *match = NULL;
> +
> + if (kv->kv_type != KEY_TYPE_CLCACHK)
> + return (0);
> +
> + if (tls_peer_cert_provided(cre->tls) != 1)
> + return (-1);
> +
> + if(strcasecmp("clhash", kv->kv_key) == 0) {
> + if(tls_peer_cert_hash(cre->tls) == NULL || \
> + strcasecmp(tls_peer_cert_hash(cre->tls), kv->kv_value) != 0)
> + return (-1);
> + } else if(strcasecmp("issuer", kv->kv_key) == 0) {
> + if(tls_peer_cert_issuer(cre->tls) == NULL || \
> + strcasecmp(tls_peer_cert_issuer(cre->tls), kv->kv_value) != 0)
> + return (-1);
> + } else if(strcasecmp("subject", kv->kv_key) == 0) {
> + if(tls_peer_cert_subject(cre->tls) == NULL || \
> + strcasecmp(tls_peer_cert_subject(cre->tls), kv->kv_value) != 0)
> + return (-1);
> + } else if(strcasecmp("name", kv->kv_key) == 0) {
> + if(!tls_peer_cert_contains_name(cre->tls, kv->kv_value))
> + return (-1);
> + }
> +
> + relay_match(actions, kv, match, NULL);
> +
> + return (0);
> +}
> +
> int
> relay_apply_actions(struct ctl_relay_event *cre, struct kvlist *actions,
> struct relay_table *tbl)
> @@ -1994,6 +2031,8 @@ relay_test(struct protocol *proto, struct ctl_relay_event *cre)
> RELAY_GET_SKIP_STEP(RULE_SKIP_METHOD);
> else if (r->rule_tagged && con->se_tag != r->rule_tagged)
> RELAY_GET_NEXT_STEP;
> + else if ((relay_clientca_test(cre, r, &matches)) != 0)
> + RELAY_GET_NEXT_STEP;
> else if (relay_httpheader_test(cre, r, &matches) != 0)
> RELAY_GET_NEXT_STEP;
> else if ((res = relay_httpquery_test(cre, r, &matches)) != 0)
> diff --git a/usr.sbin/relayd/relayd.conf.5 b/usr.sbin/relayd/relayd.conf.5
> index 8372875b853..a5b8ff314b7 100644
> --- a/usr.sbin/relayd/relayd.conf.5
> +++ b/usr.sbin/relayd/relayd.conf.5
> @@ -954,9 +954,12 @@ will be used (strong crypto cipher suites without anonymous DH).
> See the CIPHERS section of
> .Xr openssl 1
> for information about TLS cipher suites and preference lists.
> -.It Ic client ca Ar path
> +.It Ic client ca Ar path Op optional
> Require TLS client certificates that can be verified against the CA
> certificates in the specified file.
> +If the
> +.Ic optional
> +keyword is present, the certificate is verified only if presented.
> .It Ic client-renegotiation
> Allow client-initiated renegotiation.
> To mitigate a potential DoS risk,
> @@ -1213,6 +1216,27 @@ and can be either
> or
> .Ic VERSION-CONTROL .
> .It Xo
> +.It Ic client ca Op Ar option value
> +Verify if the client provided a valid certificate.
> +Valid options are:
> +.Bl -tag -width Ds
> +.It Ic clhash Ar string
> +Verify if the hash of the client certificate corresponds to the specified hash strring.
> +The hash string for a certificate in file
> +.Ar mycert.crt
> +can be generated using the commands:
> +.Bd -literal -offset indent
> +h=$(openssl x509 -outform der -in mycert.crt | sha256)
> +printf "SHA256:${h}\\n"
> +.Ed
> +.It Ic issuer Ar string
> +Verify if the issuer of the client certificate corresponds to the specified issuer string.
> +.It Ic name Ar string
> +Verify if the SAN or CN of the client certificate contains the specified name string.
> +.It Ic subject Ar string
> +Verify if the subject of the client certificate corresponds to the provided subject string.
> +.El
> +.It Xo
> .Ar type option
> .Oo Oo Ic digest Oc
> .Pq Ar key Ns | Ns Ic file Ar path
> diff --git a/usr.sbin/relayd/relayd.h b/usr.sbin/relayd/relayd.h
> index 3b5c3987f93..c1381813677 100644
> --- a/usr.sbin/relayd/relayd.h
> +++ b/usr.sbin/relayd/relayd.h
> @@ -299,6 +299,7 @@ enum key_type {
> KEY_TYPE_NONE = 0,
> KEY_TYPE_COOKIE,
> KEY_TYPE_HEADER,
> + KEY_TYPE_CLCACHK,
> KEY_TYPE_PATH,
> KEY_TYPE_QUERY,
> KEY_TYPE_URL,
> @@ -702,6 +703,7 @@ TAILQ_HEAD(relay_rules, relay_rule);
> #define TLSFLAG_VERSION 0x1f
> #define TLSFLAG_CIPHER_SERVER_PREF 0x20
> #define TLSFLAG_CLIENT_RENEG 0x40
> +#define TLSFLAG_CLIENT_OPTIONAL 0x80
> #define TLSFLAG_DEFAULT \
> (TLSFLAG_TLSV1_2|TLSFLAG_TLSV1_3|TLSFLAG_CIPHER_SERVER_PREF)
>
Relayd client ca support: optionality and filtering