Skip to content
This repository has been archived by the owner on Nov 22, 2023. It is now read-only.

Commit

Permalink
Merge pull request #792 from KenjiChao/kenjichao/configurable_auth_he…
Browse files Browse the repository at this point in the history
…ader

Add a custom header to parse the caller Spiffe Id from
  • Loading branch information
jbpeirce committed Feb 17, 2021
2 parents 55caa3d + d33bf8a commit c5bdeda
Show file tree
Hide file tree
Showing 6 changed files with 315 additions and 33 deletions.
29 changes: 29 additions & 0 deletions server/src/main/java/keywhiz/auth/mutualssl/SpiffePrincipal.java
@@ -0,0 +1,29 @@
package keywhiz.auth.mutualssl;

import java.net.URI;
import java.security.Principal;

public class SpiffePrincipal implements Principal {
private final URI spiffeId;

public SpiffePrincipal(URI spiffeId) {
this.spiffeId = spiffeId;
}

@Override public String getName() {
return spiffeId.toString();
}

public URI getSpiffeId() {
return spiffeId;
}

/**
* Use the workload id of a Spiffe Id as the client name.
*/
public String getClientName() {
String path = spiffeId.getPath();
// Drop the leading '/' character.
return path.isEmpty() ? path : path.substring(1);
}
}
19 changes: 17 additions & 2 deletions server/src/main/java/keywhiz/service/config/XfccSourceConfig.java
Expand Up @@ -20,6 +20,7 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.auto.value.AutoValue;
import java.util.List;
import javax.annotation.Nullable;

/**
* Configuration for x-forwarded-client-cert header support, as set by the Envoy proxy
Expand All @@ -30,8 +31,10 @@ public abstract class XfccSourceConfig {
@JsonCreator public static XfccSourceConfig of(
@JsonProperty("port") Integer port,
@JsonProperty("allowedClientNames") List<String> allowedClientNames,
@JsonProperty("allowedSpiffeIds") List<String> allowedSpiffeIds) {
return new AutoValue_XfccSourceConfig(port, allowedClientNames, allowedSpiffeIds);
@JsonProperty("allowedSpiffeIds") List<String> allowedSpiffeIds,
@JsonProperty("callerSpiffeIdHeader") String callerSpiffeIdHeader) {
return new AutoValue_XfccSourceConfig(port, allowedClientNames, allowedSpiffeIds,
callerSpiffeIdHeader);
}

/**
Expand All @@ -49,4 +52,16 @@ public abstract class XfccSourceConfig {
public abstract List<String> allowedClientNames();

public abstract List<String> allowedSpiffeIds();

/**
* An optional custom header identifies the "caller" who sent the original request. If present,
* we attempt to parse the client Spiffe Id from this header instead of the XFCC header.
*
* It is to support a scenario where a real client is behind a proxy. The Spiffe Id extracted from
* the XFCC header may or may not match the one set in this custom header. For example,
* considering a request flow like this: client -> proxy -> Envoy -> Keywhiz. In this case, the
* XFCC header set by the envoy instance contains the proxy cert, while the custom Spiffe Id
* header contains the client information behind the proxy.
*/
@Nullable public abstract String callerSpiffeIdHeader();
}
Expand Up @@ -35,6 +35,7 @@
import keywhiz.KeywhizConfig;
import keywhiz.api.model.Client;
import keywhiz.auth.mutualssl.CertificatePrincipal;
import keywhiz.auth.mutualssl.SpiffePrincipal;
import keywhiz.service.config.ClientAuthConfig;
import keywhiz.service.config.XfccSourceConfig;
import keywhiz.service.daos.ClientDAO;
Expand Down Expand Up @@ -107,10 +108,10 @@ public Client provide(ContainerRequest containerRequest,
// on the security context of this request
if (possibleXfccConfig.isEmpty()) {
// The XFCC header is not used; use the security context of this request to identify the client
return authenticateClientFromCertificate(requestPrincipal);
return authenticateClientFromPrincipal(requestPrincipal);
} else {
return authorizeClientFromXfccHeader(possibleXfccConfig.get(), xfccHeaderValues,
requestPrincipal);
requestPrincipal, containerRequest);
}
}

Expand All @@ -134,12 +135,34 @@ private Optional<XfccSourceConfig> getXfccConfigForPort(int port) {
}

private Client authorizeClientFromXfccHeader(XfccSourceConfig xfccConfig,
List<String> xfccHeaderValues, Principal requestPrincipal) {
List<String> xfccHeaderValues, Principal requestPrincipal,
ContainerRequest containerRequest) {
// Do not allow the XFCC header to be set by all incoming traffic. This throws a
// NotAuthorizedException when the traffic is not coming from a source allowed to set the
// header.
validateXfccHeaderAllowed(xfccConfig, requestPrincipal);

Optional<String> callerSpiffeIdHeader = Optional.ofNullable(xfccConfig.callerSpiffeIdHeader());
List<String> callerSpiffeIdList = callerSpiffeIdHeader.map(
header -> Optional.ofNullable(containerRequest.getRequestHeader(header))
.orElse(List.of()))
.orElse(List.of());
int size = callerSpiffeIdList.size();

Optional<URI> callerSpiffeId = callerSpiffeIdHeader.flatMap(
header -> ClientAuthenticator.getSpiffeIdFromHeader(containerRequest, header));

if (size > 1 || size == 1 && callerSpiffeId.isEmpty()) {
throw new NotAuthorizedException(format(
"Invalid caller Spiffe Id header. It should contain only one URI and follow Spiffe Id format. size: %d, header: %s",
size, callerSpiffeIdList));
}

if (callerSpiffeId.isPresent()) {
SpiffePrincipal spiffePrincipal = new SpiffePrincipal(callerSpiffeId.get());
return authenticateClientFromPrincipal(spiffePrincipal);
}

// Extract client information from the XFCC header
X509Certificate clientCert =
getClientCertFromXfccHeaderEnvoyFormatted(xfccHeaderValues).orElseThrow(() ->
Expand All @@ -149,9 +172,9 @@ private Client authorizeClientFromXfccHeader(XfccSourceConfig xfccConfig,

CertificatePrincipal certificatePrincipal =
new CertificatePrincipal(clientCert.getSubjectDN().toString(),
new X509Certificate[] {clientCert});
new X509Certificate[] { clientCert });

return authenticateClientFromCertificate(certificatePrincipal);
return authenticateClientFromPrincipal(certificatePrincipal);
}

private void validateXfccHeaderAllowed(XfccSourceConfig xfccConfig, Principal requestPrincipal) {
Expand Down Expand Up @@ -296,7 +319,7 @@ protected boolean createMissingClient() {
return clientAuthConfig.createMissingClients();
}

private Client authenticateClientFromCertificate(Principal clientPrincipal) {
private Client authenticateClientFromPrincipal(Principal clientPrincipal) {
Optional<Client> possibleClient =
authenticator.authenticate(clientPrincipal, createMissingClient());
return possibleClient.orElseThrow(() -> new NotAuthorizedException(
Expand Down
Expand Up @@ -12,12 +12,14 @@
import javax.ws.rs.NotAuthorizedException;
import keywhiz.api.model.Client;
import keywhiz.auth.mutualssl.CertificatePrincipal;
import keywhiz.auth.mutualssl.SpiffePrincipal;
import keywhiz.service.config.ClientAuthConfig;
import keywhiz.service.daos.ClientDAO;
import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x500.style.IETFUtils;
import org.glassfish.jersey.server.ContainerRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -120,6 +122,10 @@ private Optional<Client> handleMissingClient(boolean createMissingClient, String
}

static Optional<String> getClientName(Principal principal) {
if (principal instanceof SpiffePrincipal) {
return Optional.of(((SpiffePrincipal) principal).getClientName());
}

X500Name name = new X500Name(principal.getName());
RDN[] rdns = name.getRDNs(BCStyle.CN);
if (rdns.length == 0) {
Expand All @@ -129,20 +135,23 @@ static Optional<String> getClientName(Principal principal) {
}

static Optional<URI> getSpiffeId(Principal principal) {
if (!(principal instanceof CertificatePrincipal)) {
// A SPIFFE ID can only be parsed from a principal with a certificate
return Optional.empty();
if (principal instanceof CertificatePrincipal) {
// This chain is either from the XFCC header's "Cert" field, which includes only the
// client certificate rather than the chain, or from the CertificateSecurityContext
// configured by Keywhiz' ClientCertificateFilter, which sets it based on
// X509Certificate[] chain =
// (X509Certificate[]) context.getProperty("javax.servlet.request.X509Certificate");
// which appears to place the leaf as the zero-index entry in the chain.
X509Certificate cert = ((CertificatePrincipal) principal).getCertificateChain()
.get(0);
return getSpiffeIdFromCertificate(cert);
}

if (principal instanceof SpiffePrincipal) {
return Optional.of(((SpiffePrincipal) principal).getSpiffeId());
}

// This chain is either from the XFCC header's "Cert" field, which includes only the
// client certificate rather than the chain, or from the CertificateSecurityContext
// configured by Keywhiz' ClientCertificateFilter, which sets it based on
// X509Certificate[] chain =
// (X509Certificate[]) context.getProperty("javax.servlet.request.X509Certificate");
// which appears to place the leaf as the zero-index entry in the chain.
X509Certificate cert = ((CertificatePrincipal) principal).getCertificateChain()
.get(0);
return getSpiffeIdFromCertificate(cert);
return Optional.empty();
}

static Optional<URI> getSpiffeIdFromCertificate(X509Certificate cert) {
Expand All @@ -165,14 +174,7 @@ static Optional<URI> getSpiffeIdFromCertificate(X509Certificate cert) {
.map(sanPair -> (String) sanPair.get(1))
.collect(Collectors.toUnmodifiableList());

// https://spiffe.io/spiffe/concepts/#spiffe-verifiable-identity-document-svid
// > An SVID contains a single SPIFFE ID, which represents the identity of the service presenting it
//
// https://github.com/spiffe/spiffe/blob/master/standards/X509-SVID.md#2-spiffe-id
// > An X.509 SVID MUST contain exactly one URI SAN.
List<String> spiffeUriNames = providedUris.stream()
.filter(uri -> uri.startsWith(SPIFFE_SCHEME))
.collect(Collectors.toUnmodifiableList());
List<String> spiffeUriNames = spiffeUriNames(providedUris);

if (spiffeUriNames.size() > 1) {
logger.warn("Got multiple SPIFFE URIs from certificate: {}", spiffeUriNames);
Expand All @@ -197,4 +199,46 @@ static Optional<URI> getSpiffeIdFromCertificate(X509Certificate cert) {
}
});
}

static Optional<URI> getSpiffeIdFromHeader(ContainerRequest containerRequest,
String spiffeIdHeader) {
List<String> spiffeIdHeaderValues =
Optional.ofNullable(containerRequest.getRequestHeader(spiffeIdHeader)).orElse(List.of());
List<String> spiffeUriNames = spiffeUriNames(spiffeIdHeaderValues);

if (spiffeUriNames.isEmpty()) {
logger.warn("No SPIFFE URI found from header {}", spiffeIdHeader);
return Optional.empty();
} else if (spiffeUriNames.size() > 1) {
logger.warn("Got multiple SPIFFE URIs from header {}: {}", spiffeIdHeader, spiffeUriNames);
return Optional.empty();
} else if (spiffeIdHeaderValues.size() > 1) {
logger.warn(
"Multiple URIs are not allowed in the header {} that includes a SPIFFE URI (URIs: {})",
spiffeIdHeader, spiffeIdHeaderValues);
return Optional.empty();
}

String uri = spiffeUriNames.get(0);
try {
return Optional.of(new URI(uri));
} catch (URISyntaxException e) {
logger.warn(
format("Error parsing SPIFFE URI (%s) from the header %s as a URI", uri,
spiffeIdHeader),
e);
return Optional.empty();
}
}

private static List<String> spiffeUriNames(List<String> uris) {
// https://spiffe.io/spiffe/concepts/#spiffe-verifiable-identity-document-svid
// > An SVID contains a single SPIFFE ID, which represents the identity of the service presenting it
//
// https://github.com/spiffe/spiffe/blob/master/standards/X509-SVID.md#2-spiffe-id
// > An X.509 SVID MUST contain exactly one URI SAN.
return uris.stream()
.filter(uri -> uri.startsWith(SPIFFE_SCHEME))
.collect(Collectors.toUnmodifiableList());
}
}

0 comments on commit c5bdeda

Please sign in to comment.