[PATCH] relayd client certificate validation again
Hi! Here comes the support for relayd client certificate validation. Full certificate chain, subject and issuer can be passed over in http headers. It supports mandatory validation and optional validation(if client chooses to provide certificate it will be validated). Part of my sample config. http protocol test { match header set "CS_SUBJECT" value "$CLIENT_CERT_SUBJECT" match header set "CS_ISSUER" value "$CLIENT_CERT_ISSUER" match header set "CS_CERT" value "$CLIENT_CERT_CHAIN" pass tls {client ca "/tmp/easyrsa3/pki/ca.crt" optional } } This uses code from the patches submitted by Ashe Connor. Rivo diff refs/heads/master refs/heads/relay-clc3 blob - a2f1c130d6b45e3082048218c11537dca485998a blob + 5070a7d48f58403f53d818231e1676db749aa9d7 --- usr.sbin/relayd/config.c +++ usr.sbin/relayd/config.c @@ -954,6 +954,15 @@ config_setrelay(struct relayd *env, struct relay *rlay rlay->rl_conf.name); return (-1); } + if (rlay->rl_tls_client_ca_fd != -1 && + config_setrelayfd(ps, id, n, 0, + rlay->rl_conf.id, RELAY_FD_CLIENTCACERT, + rlay->rl_tls_client_ca_fd) == -1) { + log_warn("%s: fd passing failed for " + "`%s'", __func__, + rlay->rl_conf.name); + return (-1); + } /* Prevent fd exhaustion in the parent. */ if (proc_flush_imsg(ps, id, n) == -1) { log_warn("%s: failed to flush " @@ -987,6 +996,10 @@ config_setrelay(struct relayd *env, struct relay *rlay close(rlay->rl_s); rlay->rl_s = -1; } + if (rlay->rl_tls_client_ca_fd != -1) { + close(rlay->rl_tls_client_ca_fd); + rlay->rl_tls_client_ca_fd = -1; + } if (rlay->rl_tls_cacert_fd != -1) { close(rlay->rl_tls_cacert_fd); rlay->rl_tls_cacert_fd = -1; @@ -1012,6 +1025,10 @@ config_setrelay(struct relayd *env, struct relay *rlay cert->cert_ocsp_fd = -1; } } + if (rlay->rl_tls_client_ca_fd != -1) { + close(rlay->rl_tls_client_ca_fd); + rlay->rl_tls_client_ca_fd = -1; + } return (0); } @@ -1034,6 +1051,7 @@ config_getrelay(struct relayd *env, struct imsg *imsg) rlay->rl_s = imsg->fd; rlay->rl_tls_ca_fd = -1; rlay->rl_tls_cacert_fd = -1; + rlay->rl_tls_client_ca_fd = -1; if (ps->ps_what[privsep_process] & CONFIG_PROTOS) { if (rlay->rl_conf.proto == EMPTY_ID) @@ -1163,6 +1181,9 @@ config_getrelayfd(struct relayd *env, struct imsg *ims case RELAY_FD_CAFILE: rlay->rl_tls_cacert_fd = imsg->fd; break; + case RELAY_FD_CLIENTCACERT: + rlay->rl_tls_client_ca_fd = imsg->fd; + break; } DPRINTF("%s: %s %d received relay fd %d type %d for relay %s", __func__, blob - 22beb857229a16e5b2c17a25a2944231d41e7e08 blob + fe5e8ff4dfed10e8f09e3226bdfe33f8bc031c8e --- usr.sbin/relayd/parse.y +++ usr.sbin/relayd/parse.y @@ -172,14 +172,14 @@ typedef struct { %token CODE COOKIE DEMOTE DIGEST DISABLE ERROR EXPECT PASS BLOCK EXTERNAL %token FILENAME FORWARD FROM HASH HEADER HEADERLEN HOST HTTP ICMP INCLUDE INET %token INET6 INTERFACE INTERVAL IP KEYPAIR LABEL LISTEN VALUE LOADBALANCE LOG -%token LOOKUP METHOD MODE NAT NO DESTINATION NODELAY NOTHING ON PARENT PATH +%token LOOKUP METHOD MODE NAT NO DESTINATION NODELAY NOTHING ON OPTIONAL PARENT PATH %token PFTAG PORT PREFORK PRIORITY PROTO QUERYSTR REAL REDIRECT RELAY REMOVE %token REQUEST RESPONSE RETRY QUICK RETURN ROUNDROBIN ROUTE SACK SCRIPT SEND %token SESSION SOCKET SPLICE SSL STICKYADDR STRIP STYLE TABLE TAG TAGGED TCP %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 +%token WEBSOCKETS CLIENT %token STRING %token NUMBER %type context hostname interface table value path @@ -188,6 +188,7 @@ typedef struct { %type opttls opttlsclient %type redirect_proto relay_proto match %type action ruleaf key_option +%type clientcaopt %typeport %typehost %typeaddress rulesrc ruledst addrprefix @@ -244,6 +245,10 @@ opttlsclient : /*empty*/ { $$ = 0; } | WITH ssltls { $$ = 1; } ; +clientcaopt : /*empty*/ { $$ = 0; } + | OP
Re: [PATCH] relayd client certificate validation again
Hi, not to interrupt development … Can you make this completely optional from the servers perspective? I don’t want my endpoints validating anonymous client certificates when I run a public endpoint. I’ll just hack it out otherwise, but I think this opens a vector that should be completely optional from a relay service. > On Dec 16, 2021, at 4:25 PM, rivo nurges wrote: > > Hi! > > Here comes the support for relayd client certificate validation. > Full certificate chain, subject and issuer can be passed over in http headers. > It supports mandatory validation and optional validation(if client chooses to > provide certificate it will be validated). > > Part of my sample config. > > http protocol test { > match header set "CS_SUBJECT" value "$CLIENT_CERT_SUBJECT" > match header set "CS_ISSUER" value "$CLIENT_CERT_ISSUER" > match header set "CS_CERT" value "$CLIENT_CERT_CHAIN" > pass > tls {client ca "/tmp/easyrsa3/pki/ca.crt" optional } > } > > This uses code from the patches submitted by Ashe Connor. > > Rivo > > diff refs/heads/master refs/heads/relay-clc3 > blob - a2f1c130d6b45e3082048218c11537dca485998a > blob + 5070a7d48f58403f53d818231e1676db749aa9d7 > --- usr.sbin/relayd/config.c > +++ usr.sbin/relayd/config.c > @@ -954,6 +954,15 @@ config_setrelay(struct relayd *env, struct relay *rlay >rlay->rl_conf.name); >return (-1); >} > +if (rlay->rl_tls_client_ca_fd != -1 && > +config_setrelayfd(ps, id, n, 0, > +rlay->rl_conf.id, RELAY_FD_CLIENTCACERT, > +rlay->rl_tls_client_ca_fd) == -1) { > +log_warn("%s: fd passing failed for " > +"`%s'", __func__, > +rlay->rl_conf.name); > +return (-1); > +} >/* Prevent fd exhaustion in the parent. */ >if (proc_flush_imsg(ps, id, n) == -1) { >log_warn("%s: failed to flush " > @@ -987,6 +996,10 @@ config_setrelay(struct relayd *env, struct relay *rlay >close(rlay->rl_s); >rlay->rl_s = -1; >} > +if (rlay->rl_tls_client_ca_fd != -1) { > +close(rlay->rl_tls_client_ca_fd); > +rlay->rl_tls_client_ca_fd = -1; > +} >if (rlay->rl_tls_cacert_fd != -1) { >close(rlay->rl_tls_cacert_fd); >rlay->rl_tls_cacert_fd = -1; > @@ -1012,6 +1025,10 @@ config_setrelay(struct relayd *env, struct relay *rlay >cert->cert_ocsp_fd = -1; >} >} > +if (rlay->rl_tls_client_ca_fd != -1) { > +close(rlay->rl_tls_client_ca_fd); > +rlay->rl_tls_client_ca_fd = -1; > +} > return (0); > } > @@ -1034,6 +1051,7 @@ config_getrelay(struct relayd *env, struct imsg *imsg) >rlay->rl_s = imsg->fd; >rlay->rl_tls_ca_fd = -1; >rlay->rl_tls_cacert_fd = -1; > +rlay->rl_tls_client_ca_fd = -1; > if (ps->ps_what[privsep_process] & CONFIG_PROTOS) { >if (rlay->rl_conf.proto == EMPTY_ID) > @@ -1163,6 +1181,9 @@ config_getrelayfd(struct relayd *env, struct imsg *ims >case RELAY_FD_CAFILE: >rlay->rl_tls_cacert_fd = imsg->fd; >break; > +case RELAY_FD_CLIENTCACERT: > +rlay->rl_tls_client_ca_fd = imsg->fd; > +break; >} > DPRINTF("%s: %s %d received relay fd %d type %d for relay %s", __func__, > blob - 22beb857229a16e5b2c17a25a2944231d41e7e08 > blob + fe5e8ff4dfed10e8f09e3226bdfe33f8bc031c8e > --- usr.sbin/relayd/parse.y > +++ usr.sbin/relayd/parse.y > @@ -172,14 +172,14 @@ typedef struct { > %tokenCODE COOKIE DEMOTE DIGEST DISABLE ERROR EXPECT PASS BLOCK EXTERNAL > %tokenFILENAME FORWARD FROM HASH HEADER HEADERLEN HOST HTTP ICMP INCLUDE > INET > %tokenINET6 INTERFACE INTERVAL IP KEYPAIR LABEL LISTEN VALUE LOADBALANCE > LOG > -%tokenLOOKUP METHOD MODE NAT NO DESTINATION NODELAY NOTHING ON PARENT > PATH > +%tokenLOOKUP METHOD MODE NAT NO DESTINATION NODELAY NOTHING ON OPTIONAL > PARENT PATH > %tokenPFTAG PORT PREFORK PRIORITY PROTO QUERYSTR REAL REDIRECT RELAY > REMOVE > %tokenREQUEST RESPONSE RETRY QUICK RETURN ROUNDROBIN ROUTE SACK SCRIPT > SEND > %tokenSESSION SOCKET SPLICE SSL STICKYADDR STRIP STYLE TABLE TAG TAGGED > TCP > %tokenTIMEOUT TLS TO ROUTER RTLABEL TRANSPARENT URL WITH TTL RTABLE > %tokenMATCH PARAMS RANDOM LEASTSTATES SRCHASH KEY CERTIFICATE PASSWORD > ECDHE > %tokenEDH TICKETS CONNECTION CONNECTIONS CONTEXT ERRORS STATE CHANGES > CHECKS > -%tokenWEBSOCKETS > +%tokenWEBSOCKETS CLIENT > %tokenSTRING > %token NUMBER > %typecontext hostname interface table value path > @@ -188,6 +188,7 @@ typedef struct { > %typeopttls opttlsclient > %typeredirect_proto relay_proto match > %typeaction ruleaf key_option > +%typeclientcaopt > %typeport > %typehost > %typeaddress rulesrc ruledst addrprefix > @@
Re: [PATCH] relayd client certificate validation again
Hi, sorry for being a moron. I realize it’s already optional by not specifying client ca… sorry about the noise! > On Dec 16, 2021, at 9:35 PM, Brian Brombacher wrote: > > Hi, not to interrupt development … > > Can you make this completely optional from the servers perspective? I don’t > want my endpoints validating anonymous client certificates when I run a > public endpoint. > > I’ll just hack it out otherwise, but I think this opens a vector that should > be completely optional from a relay service. > > >> On Dec 16, 2021, at 4:25 PM, rivo nurges wrote: >> >> Hi! >> >> Here comes the support for relayd client certificate validation. >> Full certificate chain, subject and issuer can be passed over in http >> headers. >> It supports mandatory validation and optional validation(if client chooses to >> provide certificate it will be validated). >> >> Part of my sample config. >> >> http protocol test { >> match header set "CS_SUBJECT" value "$CLIENT_CERT_SUBJECT" >> match header set "CS_ISSUER" value "$CLIENT_CERT_ISSUER" >> match header set "CS_CERT" value "$CLIENT_CERT_CHAIN" >> pass >> tls {client ca "/tmp/easyrsa3/pki/ca.crt" optional } >> } >> >> This uses code from the patches submitted by Ashe Connor. >> >> Rivo >> >> diff refs/heads/master refs/heads/relay-clc3 >> blob - a2f1c130d6b45e3082048218c11537dca485998a >> blob + 5070a7d48f58403f53d818231e1676db749aa9d7 >> --- usr.sbin/relayd/config.c >> +++ usr.sbin/relayd/config.c >> @@ -954,6 +954,15 @@ config_setrelay(struct relayd *env, struct relay *rlay >> rlay->rl_conf.name); >> return (-1); >> } >> +if (rlay->rl_tls_client_ca_fd != -1 && >> +config_setrelayfd(ps, id, n, 0, >> +rlay->rl_conf.id, RELAY_FD_CLIENTCACERT, >> +rlay->rl_tls_client_ca_fd) == -1) { >> +log_warn("%s: fd passing failed for " >> +"`%s'", __func__, >> +rlay->rl_conf.name); >> +return (-1); >> +} >> /* Prevent fd exhaustion in the parent. */ >> if (proc_flush_imsg(ps, id, n) == -1) { >> log_warn("%s: failed to flush " >> @@ -987,6 +996,10 @@ config_setrelay(struct relayd *env, struct relay *rlay >> close(rlay->rl_s); >> rlay->rl_s = -1; >> } >> +if (rlay->rl_tls_client_ca_fd != -1) { >> +close(rlay->rl_tls_client_ca_fd); >> +rlay->rl_tls_client_ca_fd = -1; >> +} >> if (rlay->rl_tls_cacert_fd != -1) { >> close(rlay->rl_tls_cacert_fd); >> rlay->rl_tls_cacert_fd = -1; >> @@ -1012,6 +1025,10 @@ config_setrelay(struct relayd *env, struct relay *rlay >> cert->cert_ocsp_fd = -1; >> } >> } >> +if (rlay->rl_tls_client_ca_fd != -1) { >> +close(rlay->rl_tls_client_ca_fd); >> +rlay->rl_tls_client_ca_fd = -1; >> +} >> return (0); >> } >> @@ -1034,6 +1051,7 @@ config_getrelay(struct relayd *env, struct imsg *imsg) >> rlay->rl_s = imsg->fd; >> rlay->rl_tls_ca_fd = -1; >> rlay->rl_tls_cacert_fd = -1; >> +rlay->rl_tls_client_ca_fd = -1; >> if (ps->ps_what[privsep_process] & CONFIG_PROTOS) { >> if (rlay->rl_conf.proto == EMPTY_ID) >> @@ -1163,6 +1181,9 @@ config_getrelayfd(struct relayd *env, struct imsg *ims >> case RELAY_FD_CAFILE: >> rlay->rl_tls_cacert_fd = imsg->fd; >> break; >> +case RELAY_FD_CLIENTCACERT: >> +rlay->rl_tls_client_ca_fd = imsg->fd; >> +break; >> } >> DPRINTF("%s: %s %d received relay fd %d type %d for relay %s", __func__, >> blob - 22beb857229a16e5b2c17a25a2944231d41e7e08 >> blob + fe5e8ff4dfed10e8f09e3226bdfe33f8bc031c8e >> --- usr.sbin/relayd/parse.y >> +++ usr.sbin/relayd/parse.y >> @@ -172,14 +172,14 @@ typedef struct { >> %tokenCODE COOKIE DEMOTE DIGEST DISABLE ERROR EXPECT PASS BLOCK EXTERNAL >> %tokenFILENAME FORWARD FROM HASH HEADER HEADERLEN HOST HTTP ICMP INCLUDE >> INET >> %tokenINET6 INTERFACE INTERVAL IP KEYPAIR LABEL LISTEN VALUE LOADBALANCE >> LOG >> -%tokenLOOKUP METHOD MODE NAT NO DESTINATION NODELAY NOTHING ON PARENT >> PATH >> +%tokenLOOKUP METHOD MODE NAT NO DESTINATION NODELAY NOTHING ON OPTIONAL >> PARENT PATH >> %tokenPFTAG PORT PREFORK PRIORITY PROTO QUERYSTR REAL REDIRECT RELAY >> REMOVE >> %tokenREQUEST RESPONSE RETRY QUICK RETURN ROUNDROBIN ROUTE SACK SCRIPT >> SEND >> %tokenSESSION SOCKET SPLICE SSL STICKYADDR STRIP STYLE TABLE TAG TAGGED >> TCP >> %tokenTIMEOUT TLS TO ROUTER RTLABEL TRANSPARENT URL WITH TTL RTABLE >> %tokenMATCH PARAMS RANDOM LEASTSTATES SRCHASH KEY CERTIFICATE PASSWORD >> ECDHE >> %tokenEDH TICKETS CONNECTION CONNECTIONS CONTEXT ERRORS STATE CHANGES >> CHECKS >> -%tokenWEBSOCKETS >> +%tokenWEBSOCKETS CLIENT >> %tokenSTRING >> %token NUMBER >> %typecontext hostname interface table value path >> @
Re: [PATCH] relayd client certificate validation again
Hi, we've got the patch ready for client certificate validation, cc'ing related people. The patch adds two features: 1. client certificate validation itself 2. passing on certificate and select fields in HTTP headers ## Brief description of client certificates (for whoever else is reading) Client certificates, also known as mutual authentication, are the reverse to TLS server certificates (like letsencrypt) where the server authenticates the user instead of the other way around. In principle client certificates work as follows: - the server has keypair and a CA certificate created from this keypair - the user has a keypair and submits their public key for certification (to being signed by the CA) - the server (relayd) has the CA certificate configured as 'client ca "/path/to/ca.pem"' - the user provides their certificate when connecting, the provided certificate is validated against the CA certificate. How this is set up in practice is up to whoever implements the infrastructure. Client certificates can be installed to operating systems' certificate stores (Windows, macOS) where browsers can use them, or into browsers own certificate stores (Firefox has its own), or specified on the command line (curl, wget) etc. ## Configuration To turn on client certificate validation add tls { client ca "/path/to/ca-cert.pem" } to relayd.conf. Add "optional" flag to make the certificate not required: tls { client ca "/path/to/cert.pem" optional } With "optional" relayd will succeed in the TLS handshake when no client certificate is provided. But if a certificate *is* provided then it *must* validate with the configured CA, otherwise the TLS handshake fails. ## Pass certificate on in HTTP header To pass on the certificate in an url-encoded PEM: match header set "ANY_HEADER_NAME" value "$CLIENT_CERT_CHAIN" With this configuration the downstream can inspect the known-to-be-valid certificate further (e.g extract identity or other info from x509 extensions). There was discussion privately on is there any standard for putting certificates in HTTP headers, repeating the reply here as well: There appears to be no standard, but this is how other HTTP servers do it: - nginx urlencodes the PEM file with $ssl_client_escaped_cert[1] (this is what is done in this patch too). There is also the $ssl_client_cert variable which adds a tab to each next new line, but this way of doing multiline HTTP headers is deprecated[2]. There is also $ssl_client_raw_cert, but the raw multiline PEM is invalid HTTP header; - envoy also urlencodes the PEM[3]; - haproxy has only the binary DER, but base64 encoding it like %[ssl_c_der,base64]) should result in PEM with no newlines and no headers; - apache has the %{SSL_CLIENT_CERT} with raw (multiline) PEM[4], which is invalid in HTTP headers, but this can be processed with the escape function[5], something like "expr= %{escape:SSL_CLIENT_CERT}" [1] https://nginx.org/en/docs/http/ngx_http_ssl_module.html#var_ssl_client_escaped_cert [2] https://tools.ietf.org/html/rfc7230#section-3.2.4 [3] https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-client-cert [4] https://httpd.apache.org/docs/current/mod/mod_ssl.html#envvars [5] https://httpd.apache.org/docs/current/expr.html#functions In addition to extracting the entire certificate, subject and issuer can be put to HTTP headers too for convenience: match header set "CS_SUBJECT" value "$CLIENT_CERT_SUBJECT" match header set "CS_ISSUER" value "$CLIENT_CERT_ISSUER" -- Markus Läll On Thu, Dec 16, 2021 at 11:23 PM rivo nurges wrote: > Hi! > > Here comes the support for relayd client certificate validation. > Full certificate chain, subject and issuer can be passed over in http > headers. > It supports mandatory validation and optional validation(if client chooses > to > provide certificate it will be validated). > > Part of my sample config. > > http protocol test { >match header set "CS_SUBJECT" value "$CLIENT_CERT_SUBJECT" >match header set "CS_ISSUER" value "$CLIENT_CERT_ISSUER" >match header set "CS_CERT" value "$CLIENT_CERT_CHAIN" >pass >tls {client ca "/tmp/easyrsa3/pki/ca.crt" optional } > } > > This uses code from the patches submitted by Ashe Connor. > > Rivo > > diff refs/heads/master refs/heads/relay-clc3 > blob - a2f1c130d6b45e3082048218c11537dca485998a > blob + 5070a7d48f58403f53d818231e1676db749aa9d7 > --- usr.sbin/relayd/config.c > +++ usr.sbin/relayd/config.c > @@ -954,6 +954,15 @@ config_setrelay(struct relayd *env, struct relay *rlay > rlay->rl_conf.name); > return (-1); > } > + if (rlay->rl_tls_client_ca_fd != -1 && > + config_setrelayfd(ps, id, n, 0, > + rlay->rl_conf.id, > RELAY_FD_CLIENTCACERT, > + rlay->rl_