Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CEL functions/macros errors with tls.client placeholders #5491

Closed
ethack opened this issue Apr 13, 2023 · 11 comments
Closed

CEL functions/macros errors with tls.client placeholders #5491

ethack opened this issue Apr 13, 2023 · 11 comments
Labels
discussion 💬 The right solution needs to be found

Comments

@ethack
Copy link

ethack commented Apr 13, 2023

I'm attempting to implement some simple authorization checks using CEL and client certificate fields subject, emails, and dns_names.

What I'd really like is a reusable snippet that lets me pass in a list of names that I can then validate against the current certificate.

Something like this is what I imagine as ideal:

@auth expression {http.request.tls.client.san.emails}.exists(email, email in {args})
# or
@auth expression {args}.exists(email, email in {http.request.tls.client.san.emails})

I haven't found that it's possible to use {args} as a list, however, so I'm trying cousins of what I want as a workaround.

I'm getting several different types of errors that I believe are bugs, but may be user error. Most of my attempts involve {http.request.tls.client.*}, but I'm including an example using {http.request.method} mainly to prove to myself that something similar works.

I don't believe the examples I'm including are exhaustive. I've tried some other variations and functions and gotten similar errors.

Repro steps

I'm running Caddy using docker (in case that's relevant), but my steps are for reproducing with just the Caddy binary.

caddy version
v2.6.4 h1:2hwYqiRwk1tf3VruhMpLcYTg+11fCdr8S3jhNAdnPy8=
  • Add a /etc/host entry.
127.0.0.1 getMethod.acme.corp subjectEquals.acme.corp subjectMatches.acme.corp subjectEndsWith.acme.corp emailExistsEquals.acme.corp emailIn.acme.corp domainExistsEquals.acme.corp domainExistsEndsWith.acme.corp
  • Place root cert in /etc/caddy/certs/root_ca.crt (or change Caddyfile path to match).
  • Place rest of certs/keys in current directory (or change curl command arguments to match).
  • Place Caddy config in /etc/caddy/Caddyfile (or change caddy command argument to match).

Start Caddy.

caddy run --config /etc/caddy/Caddyfile --adapter caddyfile

Use curl to access sites and submit client certificates.

# Works
❯ curl -k --cert user.crt --key user.key https://getMethod.acme.corp/
Authorized.

# Works as intended (POST method not authorized)
❯ curl -k --cert user.crt --key user.key https://getMethod.acme.corp/ -X POST

# Works
❯ curl -k --cert user.crt --key user.key https://subjectEquals.acme.corp/
Authorized.

# Works as intended (device cert not authorized)
❯ curl -k --cert device.crt --key device.key https://subjectEquals.acme.corp/

# Caddy produces errors
❯ curl -k --cert user.crt --key user.key https://subjectMatches.acme.corp/

{"level":"error","ts":1681344942.8707256,"logger":"http.matchers.expression","msg":"evaluating expression","error":"no such overload"}
{"level":"error","ts":1681344942.870744,"logger":"http.log.error","msg":"no such overload","request":{"remote_ip":"172.23.0.1","remote_port":"55492","proto":"HTTP/2.0","method":"GET","host":"subjectMatches.acme.corp","uri":"/","headers":{"User-Agent":["curl/7.81.0"],"Accept":["*/*"]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"h2","server_name":"subjectmatches.acme.corp","client_common_name":"[email protected]","client_serial":"269261576516162278322897936109726082507"}},"duration":0.000040554}

# Caddy produces errors
❯ curl -k --cert user.crt --key user.key https://subjectEndsWith.acme.corp/

{"level":"error","ts":1681344989.5696084,"logger":"http.matchers.expression","msg":"evaluating expression","error":"internal error: interface conversion: caddyhttp.celPkixName is not traits.Receiver: missing method Receive"}
{"level":"error","ts":1681344989.5696514,"logger":"http.log.error","msg":"internal error: interface conversion: caddyhttp.celPkixName is not traits.Receiver: missing method Receive","request":{"remote_ip":"172.23.0.1","remote_port":"47960","proto":"HTTP/2.0","method":"GET","host":"subjectEndsWith.acme.corp","uri":"/","headers":{"User-Agent":["curl/7.81.0"],"Accept":["*/*"]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"h2","server_name":"subjectendswith.acme.corp","client_common_name":"[email protected]","client_serial":"269261576516162278322897936109726082507"}},"duration":0.00008581}

# Works
❯ curl -k --cert user.crt --key user.key https://emailIn.acme.corp/
Authorized. 

The commented blocks in the Caddyfile all produce errors when I start the server and Caddy parses the config. They all basically say the same thing. I admit I could be doing something wrong, but if I try replacing the placeholder with a literal list then it works.

Error: loading initial config: loading new config: loading http app module: provision http: server srv0: setting up route handlers: route 0: loading handler modules: position 0: loading module 'subroute': provision http.handlers.subroute: setting up subroutes: route 0: loading matcher modules: module name 'expression': provision http.matchers.expression: compiling CEL program: ERROR: <input>:1:17: expression of type 'any' cannot be range of a comprehension (must be list, map, or dynamic)
 | caddyPlaceholder(request, "http.request.tls.client.san.emails").exists(email, email == "[email protected]" )
 | ................^

Error: loading initial config: loading new config: loading http app module: provision http: server srv0: setting up route handlers: route 0: loading handler modules: position 0: loading module 'subroute': provision http.handlers.subroute: setting up subroutes: route 0: loading matcher modules: module name 'expression': provision http.matchers.expression: compiling CEL program: ERROR: <input>:1:17: expression of type 'any' cannot be range of a comprehension (must be list, map, or dynamic)
 | caddyPlaceholder(request, "http.request.tls.client.san.dns_names").exists(domain, domain == "device.acme.corp" )
 | ................^

Error: loading initial config: loading new config: loading http app module: provision http: server srv0: setting up route handlers: route 0: loading handler modules: position 0: loading module 'subroute': provision http.handlers.subroute: setting up subroutes: route 0: loading matcher modules: module name 'expression': provision http.matchers.expression: compiling CEL program: ERROR: <input>:1:17: expression of type 'any' cannot be range of a comprehension (must be list, map, or dynamic)
 | caddyPlaceholder(request, "http.request.tls.client.san.dns_names").exists(domain, domain.endsWith("device.acme.corp"))
 | ................^

Supporting Files

certs.zip (I just realized these expire in 24 hours. Ask if you'd like new ones.)

Caddyfile

(enforce_mtls) {
	tls internal {
		client_auth {
			mode require_and_verify
			trusted_ca_cert_file /etc/caddy/certs/root_ca.crt
		}
	}

	@getMethod expression {http.request.method}.startsWith("GET") # works
	@subjectEquals expression {http.request.tls.client.subject} == "[email protected]" # works
	@subjectMatches expression {http.request.tls.client.subject}.matches("@acme\\.corp$") # doesn't work
	@subjectEndsWith expression {http.request.tls.client.subject}.endsWith("@acme.corp") # doesn't work
	@emailIn expression "[email protected]" in {http.request.tls.client.san.emails} # works
	# @emailExistsEquals expression {http.request.tls.client.san.emails}.exists(email, email == "[email protected]") # error
	# @domainExistsEquals expression {http.request.tls.client.san.dns_names}.exists(domain, domain == "device.acme.corp") # error
	# @domainExistsEndsWith expression {http.request.tls.client.san.dns_names}.exists(domain, domain.endsWith("device.acme.corp")) # error

	# @emailExistsEquals expression ["[email protected]", "[email protected]"].exists(email, email == "[email protected]") # literal list works
}

getMethod.acme.corp {
	import enforce_mtls
	respond @getMethod "Authorized."
}

subjectEquals.acme.corp {
	import enforce_mtls
	respond @subjectEquals "Authorized."
}

subjectMatches.acme.corp {
	import enforce_mtls
	respond @subjectMatches "Authorized."
}

subjectEndsWith.acme.corp {
	import enforce_mtls
	respond @subjectEndsWith "Authorized."
}

emailIn.acme.corp {
	import enforce_mtls
	respond @emailIn "Authorized."
}

# emailExistsEquals.acme.corp {
# 	import enforce_mtls
# 	respond @emailExistsEquals "Authorized."
# }

# domainExistsEquals.acme.corp {
# 	import enforce_mtls
# 	respond @domainExistsEquals "Authorized."
# }

# domainExistsEndsWith.acme.corp {
# 	import enforce_mtls
# 	respond @domainExistsEndsWith "Authorized."
# }

References

  • https://github.com/google/cel-spec/blob/master/doc/langdef.md#macros
  • https://github.com/google/cel-spec/blob/master/doc/langdef.md#list-of-standard-definitions
  • https://cloud.google.com/certificate-authority-service/docs/using-cel
  • // App is a robust, production-ready HTTP server.
    //
    // HTTPS is enabled by default if host matchers with qualifying names are used
    // in any of routes; certificates are automatically provisioned and renewed.
    // Additionally, automatic HTTPS will also enable HTTPS for servers that listen
    // only on the HTTPS port but which do not have any TLS connection policies
    // defined by adding a good, default TLS connection policy.
    //
    // In HTTP routes, additional placeholders are available (replace any `*`):
    //
    // Placeholder | Description
    // ------------|---------------
    // `{http.request.body}` | The request body (⚠️ inefficient; use only for debugging)
    // `{http.request.cookie.*}` | HTTP request cookie
    // `{http.request.duration}` | Time up to now spent handling the request (after decoding headers from client)
    // `{http.request.duration_ms}` | Same as 'duration', but in milliseconds.
    // `{http.request.uuid}` | The request unique identifier
    // `{http.request.header.*}` | Specific request header field
    // `{http.request.host}` | The host part of the request's Host header
    // `{http.request.host.labels.*}` | Request host labels (0-based from right); e.g. for foo.example.com: 0=com, 1=example, 2=foo
    // `{http.request.hostport}` | The host and port from the request's Host header
    // `{http.request.method}` | The request method
    // `{http.request.orig_method}` | The request's original method
    // `{http.request.orig_uri}` | The request's original URI
    // `{http.request.orig_uri.path}` | The request's original path
    // `{http.request.orig_uri.path.*}` | Parts of the original path, split by `/` (0-based from left)
    // `{http.request.orig_uri.path.dir}` | The request's original directory
    // `{http.request.orig_uri.path.file}` | The request's original filename
    // `{http.request.orig_uri.query}` | The request's original query string (without `?`)
    // `{http.request.port}` | The port part of the request's Host header
    // `{http.request.proto}` | The protocol of the request
    // `{http.request.remote.host}` | The host (IP) part of the remote client's address
    // `{http.request.remote.port}` | The port part of the remote client's address
    // `{http.request.remote}` | The address of the remote client
    // `{http.request.scheme}` | The request scheme
    // `{http.request.tls.version}` | The TLS version name
    // `{http.request.tls.cipher_suite}` | The TLS cipher suite
    // `{http.request.tls.resumed}` | The TLS connection resumed a previous connection
    // `{http.request.tls.proto}` | The negotiated next protocol
    // `{http.request.tls.proto_mutual}` | The negotiated next protocol was advertised by the server
    // `{http.request.tls.server_name}` | The server name requested by the client, if any
    // `{http.request.tls.client.fingerprint}` | The SHA256 checksum of the client certificate
    // `{http.request.tls.client.public_key}` | The public key of the client certificate.
    // `{http.request.tls.client.public_key_sha256}` | The SHA256 checksum of the client's public key.
    // `{http.request.tls.client.certificate_pem}` | The PEM-encoded value of the certificate.
    // `{http.request.tls.client.certificate_der_base64}` | The base64-encoded value of the certificate.
    // `{http.request.tls.client.issuer}` | The issuer DN of the client certificate
    // `{http.request.tls.client.serial}` | The serial number of the client certificate
    // `{http.request.tls.client.subject}` | The subject DN of the client certificate
    // `{http.request.tls.client.san.dns_names.*}` | SAN DNS names(index optional)
    // `{http.request.tls.client.san.emails.*}` | SAN email addresses (index optional)
    // `{http.request.tls.client.san.ips.*}` | SAN IP addresses (index optional)
    // `{http.request.tls.client.san.uris.*}` | SAN URIs (index optional)
    // `{http.request.uri}` | The full request URI
    // `{http.request.uri.path}` | The path component of the request URI
    // `{http.request.uri.path.*}` | Parts of the path, split by `/` (0-based from left)
    // `{http.request.uri.path.dir}` | The directory, excluding leaf filename
    // `{http.request.uri.path.file}` | The filename of the path, excluding directory
    // `{http.request.uri.query}` | The query string (without `?`)
    // `{http.request.uri.query.*}` | Individual query string value
    // `{http.response.header.*}` | Specific response header field
    // `{http.vars.*}` | Custom variables in the HTTP handler chain
    // `{http.shutting_down}` | True if the HTTP app is shutting down
    // `{http.time_until_shutdown}` | Time until HTTP server shutdown, if scheduled
  • if strings.HasPrefix(field, "client.") {
    cert := getTLSPeerCert(req.TLS)
    if cert == nil {
    return nil, false
    }
    // subject alternate names (SANs)
    if strings.HasPrefix(field, "client.san.") {
    field = field[len("client.san."):]
    var fieldName string
    var fieldValue any
    switch {
    case strings.HasPrefix(field, "dns_names"):
    fieldName = "dns_names"
    fieldValue = cert.DNSNames
    case strings.HasPrefix(field, "emails"):
    fieldName = "emails"
    fieldValue = cert.EmailAddresses
    case strings.HasPrefix(field, "ips"):
    fieldName = "ips"
    fieldValue = cert.IPAddresses
    case strings.HasPrefix(field, "uris"):
    fieldName = "uris"
    fieldValue = cert.URIs
    default:
    return nil, false
    }
    field = field[len(fieldName):]
    // if no index was specified, return the whole list
    if field == "" {
    return fieldValue, true
    }
    if len(field) < 2 || field[0] != '.' {
    return nil, false
    }
    field = field[1:] // trim '.' between field name and index
    // get the numeric index
    idx, err := strconv.Atoi(field)
    if err != nil || idx < 0 {
    return nil, false
    }
    // access the indexed element and return it
    switch v := fieldValue.(type) {
    case []string:
    if idx >= len(v) {
    return nil, true
    }
    return v[idx], true
    case []net.IP:
    if idx >= len(v) {
    return nil, true
    }
    return v[idx], true
    case []*url.URL:
    if idx >= len(v) {
    return nil, true
    }
    return v[idx], true
    }
    }
    switch field {
    case "client.fingerprint":
    return fmt.Sprintf("%x", sha256.Sum256(cert.Raw)), true
    case "client.public_key", "client.public_key_sha256":
    if cert.PublicKey == nil {
    return nil, true
    }
    pubKeyBytes, err := marshalPublicKey(cert.PublicKey)
    if err != nil {
    return nil, true
    }
    if strings.HasSuffix(field, "_sha256") {
    return fmt.Sprintf("%x", sha256.Sum256(pubKeyBytes)), true
    }
    return fmt.Sprintf("%x", pubKeyBytes), true
    case "client.issuer":
    return cert.Issuer, true
    case "client.serial":
    return cert.SerialNumber, true
    case "client.subject":
    return cert.Subject, true
    case "client.certificate_pem":
    block := pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}
    return pem.EncodeToMemory(&block), true
    case "client.certificate_der_base64":
    return base64.StdEncoding.EncodeToString(cert.Raw), true
    default:
    return nil, false
    }
    }
@francislavoie
Copy link
Member

In the next version, it will be possible to use variadic args. See #5249. The syntax is approximately the same as Go's slice syntax with an upper and lower bound index into the args.

You can build from source to try this out if you like. You can do xcaddy build master in your Dockerfile.

Regarding your CEL compile error, essentially that's a type error. It's saying that the type of the placeholder (which is turned into a function call with regexp replacement ahead of compiling) has the return type any because what it returns is unknown, but a known list type is required to compile an "exists" statement. Not much we can do about that I think, that's just how CEL works. Maybe @TristonianJones has some thoughts though.

But with variadic args you can probably ensure it's always a list by doing like [{args[0:]}] or whatever.

@ethack
Copy link
Author

ethack commented Apr 13, 2023

Thanks! I didn't know variadic args were coming and now I'm looking forward to that. I'll try out the master branch and I think I can work around for what I want to accomplish.

Regarding the errors, I realize they are spread out and buried in my post so I'll summarize the three types:

  • {http.request.tls.client.subject}.matches("@acme.corp") => no such overload
    • This one isn't a CEL compile error because it only happens at request time. But my interpretation is that matches requires {http.request.tls.client.subject} to be a string and since it's not there's no function overload that matches the argument types.
    • What's confusing to me is why {http.request.method}.matches("GET") works fine (I tested it separately). Why are {http.request.tls.client.subject} and {http.request.method} treated differently when, from my reading, they are both returned as the any type? Could it be that method goes down a different codepath and gets cast to a string while subject doesn't?
  • {http.request.tls.client.subject}.endsWith("@acme.corp") => internal error: interface conversion: caddyhttp.celPkixName is not traits.Receiver: missing method Receive
    • I don't know what to make of this one. {http.request.method}.endsWith("GET") works fine and I'd understand if I got the same no such overload error as matches above.
    • If I replace with the literal subject from the certificate it works "[email protected]".endsWith("@acme.corp"). And an equality check works {http.request.tls.client.subject} == "[email protected]".
  • {http.request.tls.client.san.emails}.exists(email, email == "[email protected]") => expression of type 'any' cannot be range of a comprehension (must be list, map, or dynamic)
    • I think this is the error you were referring to in your response a known list type is required to compile an "exists" statement. As far as what Caddy could do, is there a spot before they are used that these values (emails, dns_names, ips, urls) could be cast to an array since that is what they should always be?
    • It's interesting that "[email protected]" in {http.request.tls.client.san.emails} works. Something must be different either in the handling of the value's type or in the requirements for the in operator.

@francislavoie
Copy link
Member

francislavoie commented Apr 13, 2023

{http.request.tls.client.subject} returns a pkix.Name type, not a string. That type is a struct: https://pkg.go.dev/crypto/x509/pkix#Name. It does have a String() function though so in theory it should be possible to cast to a string, but I don't think that happens in CEL expressions. I'm not sure how we would fix that though. Maybe it's possible to type convert to a string, but I'm not sure of the syntax. Maybe wrapping it in string() would work? 🤷‍♂️

Re "receiver", that's talking about https://github.com/google/cel-spec/blob/master/doc/langdef.md#receiver-call-style. That's interesting though, caddyhttp.celPkixName made me realize we have some special code in Caddy to wrap that type for use in expressions. I guess that code is incomplete 🤔

func (celPkixName) ConvertToType(typeVal ref.Type) ref.Val {
	panic("not implemented")
}

I think this should support a cast to string, that would be sensible. Maybe something like this:

func (pn celPkixName) ConvertToType(typeVal ref.Type) ref.Val {
	if typeVal.TypeName() == "string" {
		return types.String(pn.Name.String())
	}
	panic("not implemented")
}

I can open a PR with this change if you want to try it out. (Edit: see #5492)

For {http.request.tls.client.san.emails}, I'm not sure what the deal is there. I think we'll need help from @TristonianJones to explain that one.

@TristonianJones
Copy link
Contributor

TristonianJones commented Apr 13, 2023

For {http.request.tls.client.san.emails}.exists(email, email == "[email protected]"), I think the issue is that the type for the left-hand side is being flagged as an Any value rather than as a dyn value. In theory this is a simple change in how the type is declared within Caddy server.

For string conversion, you might also have to hack it a bit since it's not easy to change the declaration set for the builtin types (not yet anyway):

For your custom function, you'd want the following which would be more CEL idiomatic:

func (pn celPkixName) ConvertToType(typeVal ref.Type) ref.Val {
	if typeVal.TypeName() == "string" {
		return types.String(pn.Name.String())
	}
        if typeVal.TypeName() == "type" {
             return pn.Type()
        }
	return types.ValOrErr(typeVal, "no such overload")
}

To get the type-checker to pass, you might need to use the dyn() function to get the type-checker to ignore that the input to the string conversion function isn't in the standard CEL declarations: string(dyn({http.request.tls.client.subject})). I would need to double-check to recall for sure. We did some work to make this case of adding additional conversion overloads simpler, but I can't remember how much simpler we made it.

@francislavoie
Copy link
Member

Hmm, that code doesn't seem right @TristonianJones, both those parts give errors:

cannot use pn.Type() (value of type ref.Type) as ref.Val value in return statement: ref.Type does not implement ref.Val (missing method ConvertToNative) compiler(InvalidIfaceAssign)

cannot use typeVal (variable of type ref.Type) as ref.Val value in argument to types.ValOrErr: ref.Type does not implement ref.Val (missing method ConvertToNative) compiler(InvalidIfaceAssign)

@ethack
Copy link
Author

ethack commented Apr 17, 2023

I tried out the variadic args from #5249 but so far no luck. If I should make a separate issue to discuss this feature let me know.

caddy version
v2.6.5-0.20230415013833-998c6e06a750 h1:iUTzAcbFHXDMxjy9ydfV/znP/gCnmq6TgvVZnNGE/LY=
(enforce_mtls) {
	tls internal {
		client_auth {
			mode require_and_verify
			trusted_ca_cert_file /etc/caddy/certs/root_ca.crt
		}
	}

	@user expression [{args[:]}].exists(email, email in {http.request.tls.client.san.emails})
}

user.acme.corp {
	import enforce_mtls "[email protected]"
	respond @user "Authorized. Welcome, {http.request.tls.client.subject}."
}

Error on starting Caddy:

ERROR: <input>:1:10: Syntax error: mismatched input '}' expecting ':'
  | [{args[:]}].exists(email, email in caddyPlaceholder(request, "http.request.tls.client.san.emails"))
  | .........^

Similar results for all the passing test cases from that pull request:

  • [{args[0:]}]
  • [{args[:0]}]
  • [{args[0:1]}]
  • {args[:]}

I'm able to get ["{args[0]}"].exists(email, email in {http.request.tls.client.san.emails}) working, but that brings me back to where I started.


(I also made a comment here regarding the proposed caddyhttp.celPkixName is not traits.Receiver fix.)

@francislavoie
Copy link
Member

francislavoie commented Apr 18, 2023

Hmm, I'm confused. I'm not sure why the args didn't get replaced. Can you run caddy adapt --pretty --config /path/to/Caddyfile and show what you get?

Edit: Oh, right. Variadic placeholders only get expanded if they're a token on their own (i.e. starts with {args[ and ends with ]}. The reasoning was that we want to replace one token with many tokens, and replacing it into a single token doesn't make sense.

One dumb workaround is to put spaces around it so like [ {args[:]} ].exists(...) (and you can't use the expression matcher shortcut with backticks because that counts as a single token) but this isn't usable anyways because it won't join the args with , and it won't wrap them with "" either. There's no good way to convert a list of args into a list of CEL strings. Maybe Something insane like " {args[:]} ".trim().split(' ').exists(...) could work, but neither trim nor split exist as CEL built-ins I think.

@ethack
Copy link
Author

ethack commented Apr 18, 2023

Ah, ok. I see the warning now that I run caddy adapt. It was probably there before, but I had missed/overlooked it.

2023/04/18 01:06:09.728 WARN    caddyfile       Variadic placeholder {args[:]} must be a token on its own

FWIW, here's the relevant portion it generates.

"match": [
        {
                "expression": "[{args[:]}].exists(email, email in {http.request.tls.client.san.emails})"
        }
]

With spaces it does the expansion but, as you said, there are no quotes or commas to make it valid list syntax.

Thanks for the explanation!

@francislavoie
Copy link
Member

Oh I guess this could work:

(enforce_mtls) {
	@user `{args[0]}.exists(email, email in {http.request.tls.client.san.emails})`
}

user.acme.corp {
	import enforce_mtls `['[email protected]', '[email protected]']`
	respond @user "Authorized. Welcome, {http.request.tls.client.subject}."
}

You just need to make sure to specify the import arg as a valid CEL array as a single-token-string.


So with the above solution and #5492, are all your concerns satisfied? I'm not sure where we're at now. Anything left?

@ethack
Copy link
Author

ethack commented Apr 19, 2023

Nice! That works.

Just to make sure I understand it, the backticks in the import statement are to allow spaces within a single "token" right? Without them, Caddy will separate tokens on whitespace?

I ask because that's my understanding and both of these seem to work. I removed the space in the second line to make it all one token without the backticks needed.

import enforce_mtls `['[email protected]', '[email protected]']`
import enforce_mtls ['[email protected]','[email protected]']

I'm guessing using backticks is preferred though to avoid unintentionally creating multiple tokens.

Out of curiosity, is there a difference between single quote ' and double quote "?


I'm satisfied with the solutions here. Here's my summary for anyone finding this in the future. This is written with the expression matcher and CEL in mind. Uses the new {args[0]} syntax introduced in #5249.

  • {args[:]} is a new variadic feature, but is just inserted as a series of tokens and isn't interpreted as a list.
  • You can make CEL interpret a snippet argument as a list by just quoting it to make sure the list is a single token and then using {args[0]}.
  • {http.request.tls.client.san.*} are of type any and need to by cast using dyn(...) in order to be used with macros like .exists().
  • {http.request.tls.client.subject} is a pkix.Name type and needs to be cast using string(...) in order to be used with functions like .endsWith().

Examples

Do This: (requires #5249, otherwise use {args.0} syntax)

(enforce_mtls) {
        # The value in {args[0]} will be inserted in the CEL expression and interpreted as a list by the CEL parser
	@user expression {args[0]}.exists(email, email in {http.request.tls.client.san.emails})
        # This also works
        #@user expression dyn({http.request.tls.client.san.emails}).exists(email, email in {args[0})
}

user.acme.corp {
        # Pass a list as a single token to the enforce_mtls snippet
	import enforce_mtls `['[email protected]', '[email protected]']`
	respond @user "Authorized. Welcome, {http.request.tls.client.subject}."
}

Not This:

(enforce_mtls) {
        @user expression {args[:]}.exists(email, email in {http.request.tls.client.san.emails})
        @user expression [{args[:]}].exists(email, email in {http.request.tls.client.san.emails})
        @user expression [ {args[:]} ].exists(email, email in {http.request.tls.client.san.emails})
        @user expression {http.request.tls.client.san.emails}.exists(email, email in {args[:]})
}

user.acme.corp {
        # Attempt at using variadic args
	import enforce_mtls [email protected] [email protected]
	respond @user "Authorized. Welcome, {http.request.tls.client.subject}."
}

Do This: (requires #5492)

@subjectMatches expression string({http.request.tls.client.subject}).matches("@acme\\.corp$")
@subjectEndsWith expression string({http.request.tls.client.subject}).endsWith("@acme.corp")

Not This:

@subjectMatches expression {http.request.tls.client.subject}.matches("@acme\\.corp$")
@subjectEndsWith expression {http.request.tls.client.subject}.endsWith("@acme.corp")

Thank you for all the help!

@ethack ethack closed this as completed Apr 19, 2023
@francislavoie francislavoie added the discussion 💬 The right solution needs to be found label Apr 19, 2023
@francislavoie
Copy link
Member

Just to make sure I understand it, the backticks in the import statement are to allow spaces within a single "token" right? Without them, Caddy will separate tokens on whitespace?

Correct, the Caddyfile considers spaces the end of a token, unless quoted. Either " or backticks can be used in the Caddyfile for tokens.

Out of curiosity, is there a difference between single quote ' and double quote "?

CEL supports both interchangeably. I tend to use ' because it "avoids the risk" of conflicting with the Caddyfile " for tokens. Using backticks gets around that concern, but 🤷‍♂️ no harm in using both to avoid mistakes.

Uses the new {args[0]} syntax introduced in #5249.

Btw you can still use {args.0} for now, but we deprecated that syntax (deprecation warning will appear in the next release) because we introduced the new syntax which is more natural along with variadic support.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion 💬 The right solution needs to be found
Projects
None yet
Development

No branches or pull requests

3 participants