diff options
16 files changed, 233 insertions, 404 deletions
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java index f5dbcb6a699..531a815922b 100644 --- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java +++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/restapi/CertificateAuthorityApiHandler.java @@ -37,7 +37,6 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Function; import java.util.logging.Level; -import java.util.stream.Stream; /** * REST API for issuing and refreshing node certificates in a hosted Vespa system. @@ -177,9 +176,7 @@ public class CertificateAuthorityApiHandler extends ThreadedHttpRequestHandler { private AthenzService getRequestAthenzService(HttpRequest request) { return getRequestCertificateChain(request).stream() .findFirst() - .map(X509CertificateUtils::getSubjectCommonNames) - .map(List::stream) - .flatMap(Stream::findFirst) + .flatMap(X509CertificateUtils::getSubjectCommonName) .map(AthenzService::new) .orElseThrow(() -> new RuntimeException("No certificate found")); } diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/security/MultiTenantRpcAuthorizerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/security/MultiTenantRpcAuthorizerTest.java index bffed6eb0b1..2ab959fcaa0 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/security/MultiTenantRpcAuthorizerTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/security/MultiTenantRpcAuthorizerTest.java @@ -18,6 +18,7 @@ import com.yahoo.security.KeyAlgorithm; import com.yahoo.security.KeyUtils; import com.yahoo.security.SignatureAlgorithm; import com.yahoo.security.X509CertificateBuilder; +import com.yahoo.security.tls.CapabilityMode; import com.yahoo.security.tls.CapabilitySet; import com.yahoo.security.tls.ConnectionAuthContext; import com.yahoo.slime.Cursor; @@ -250,7 +251,7 @@ public class MultiTenantRpcAuthorizerTest { private static Request mockJrtRpcRequest(String payload) { ConnectionAuthContext authContext = - new ConnectionAuthContext(PEER_CERTIFICATE_CHAIN, CapabilitySet.all(), Set.of()); + new ConnectionAuthContext(PEER_CERTIFICATE_CHAIN, CapabilitySet.all(), Set.of(), CapabilityMode.ENFORCE); Target target = mock(Target.class); when(target.connectionAuthContext()).thenReturn(authContext); Request request = mock(Request.class); diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java index 0a8733fb124..c6238149be1 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java @@ -471,6 +471,14 @@ public class Flags { "Takes effect on next tick", HOSTNAME); + public static final UnboundStringFlag TLS_CAPABILITIES_ENFORCEMENT_MODE = defineStringFlag( + "tls-capabilities-enforcement-mode", "disable", + List.of("bjorncs", "vekterli"), "2022-07-21", "2024-01-01", + "Configure Vespa TLS capability enforcement mode", + "Takes effect on restart of Docker container", + APPLICATION_ID,HOSTNAME,NODE_TYPE,TENANT_ID,VESPA_VERSION + ); + /** WARNING: public for testing: All flags should be defined in {@link Flags}. */ public static UnboundBooleanFlag defineFeatureFlag(String flagId, boolean defaultValue, List<String> owners, String createdAt, String expiresAt, String description, diff --git a/jrt/src/com/yahoo/jrt/RequireCapabilitiesFilter.java b/jrt/src/com/yahoo/jrt/RequireCapabilitiesFilter.java index bb2eafcf711..9bb497e96ed 100644 --- a/jrt/src/com/yahoo/jrt/RequireCapabilitiesFilter.java +++ b/jrt/src/com/yahoo/jrt/RequireCapabilitiesFilter.java @@ -2,24 +2,14 @@ package com.yahoo.jrt; import com.yahoo.security.tls.Capability; -import com.yahoo.security.tls.CapabilityMode; import com.yahoo.security.tls.CapabilitySet; -import com.yahoo.security.tls.ConnectionAuthContext; -import com.yahoo.security.tls.TransportSecurityUtils; - -import java.util.logging.Logger; - -import static com.yahoo.security.tls.CapabilityMode.DISABLE; -import static com.yahoo.security.tls.CapabilityMode.LOG_ONLY; +import com.yahoo.security.tls.MissingCapabilitiesException; /** * @author bjorncs */ public class RequireCapabilitiesFilter implements RequestAccessFilter { - private static final Logger log = Logger.getLogger(RequireCapabilitiesFilter.class.getName()); - private static final CapabilityMode MODE = TransportSecurityUtils.getCapabilityMode(); - private final CapabilitySet requiredCapabilities; public RequireCapabilitiesFilter(CapabilitySet requiredCapabilities) { @@ -32,23 +22,13 @@ public class RequireCapabilitiesFilter implements RequestAccessFilter { @Override public boolean allow(Request r) { - if (MODE == DISABLE) return true; - ConnectionAuthContext ctx = r.target().connectionAuthContext(); - CapabilitySet peerCapabilities = ctx.capabilities(); - boolean authorized = peerCapabilities.has(requiredCapabilities); - if (!authorized) { - String msg = "%sPermission denied for RPC method '%s'. Peer at %s with %s. Call requires %s, but peer has %s" - .formatted(MODE == LOG_ONLY ? "Dry-run: " : "", r.methodName(), r.target().peerSpec(), ctx.peerCertificateString().orElseThrow(), - requiredCapabilities.toNames(), peerCapabilities.toNames()); - if (MODE == LOG_ONLY) { - log.info(msg); - return true; - } else { - log.warning(msg); - return false; - } + try { + r.target().connectionAuthContext() + .verifyCapabilities(requiredCapabilities, "RPC", r.methodName(), r.target().peerSpec().toString()); + return true; + } catch (MissingCapabilitiesException e) { + return false; } - return true; } } diff --git a/jrt/src/com/yahoo/jrt/TlsCryptoSocket.java b/jrt/src/com/yahoo/jrt/TlsCryptoSocket.java index 13274dc3ba5..d83c1ee8baa 100644 --- a/jrt/src/com/yahoo/jrt/TlsCryptoSocket.java +++ b/jrt/src/com/yahoo/jrt/TlsCryptoSocket.java @@ -2,7 +2,8 @@ package com.yahoo.jrt; import com.yahoo.security.tls.ConnectionAuthContext; -import com.yahoo.security.tls.PeerAuthorizerTrustManager; +import com.yahoo.security.tls.PeerAuthorizationFailedException; +import com.yahoo.security.tls.TransportSecurityUtils; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; @@ -97,15 +98,6 @@ public class TlsCryptoSocket implements CryptoSocket { channelRead(); break; case NEED_WORK: - if (authContext == null) { - PeerAuthorizerTrustManager.getConnectionAuthContext(sslEngine) // only available during handshake - .ifPresent(ctx -> { - if (!ctx.authorized()) { - metrics.incrementPeerAuthorizationFailures(); - } - authContext = ctx; - }); - } break; case COMPLETED: return HandshakeState.COMPLETED; @@ -122,6 +114,10 @@ public class TlsCryptoSocket implements CryptoSocket { SSLSession session = sslEngine.getSession(); sessionApplicationBufferSize = session.getApplicationBufferSize(); sessionPacketBufferSize = session.getPacketBufferSize(); + authContext = TransportSecurityUtils.getConnectionAuthContext(session).orElseThrow(); + if (!authContext.authorized()) { + metrics.incrementPeerAuthorizationFailures(); + } log.fine(() -> String.format("Handshake complete: protocol=%s, cipherSuite=%s", session.getProtocol(), session.getCipherSuite())); if (sslEngine.getUseClientMode()) { metrics.incrementClientTlsConnectionsEstablished(); @@ -143,8 +139,7 @@ public class TlsCryptoSocket implements CryptoSocket { } } } catch (SSLHandshakeException e) { - // sslEngine.getDelegatedTask().run() and handshakeWrap() may throw SSLHandshakeException, potentially handshakeUnwrap() and sslEngine.beginHandshake() as well. - if (authContext == null || authContext.authorized()) { // don't include handshake failures due from PeerAuthorizerTrustManager + if (!(e.getCause() instanceof PeerAuthorizationFailedException)) { metrics.incrementTlsCertificateVerificationFailures(); } throw e; diff --git a/security-utils/src/main/java/com/yahoo/security/tls/CapabilitySet.java b/security-utils/src/main/java/com/yahoo/security/tls/CapabilitySet.java index ec402719efa..7e6c7f394cd 100644 --- a/security-utils/src/main/java/com/yahoo/security/tls/CapabilitySet.java +++ b/security-utils/src/main/java/com/yahoo/security/tls/CapabilitySet.java @@ -30,15 +30,17 @@ public class CapabilitySet { ; private final String name; - private final EnumSet<Capability> caps; + private final CapabilitySet set; Predefined(String name, Capability... caps) { this.name = name; - this.caps = caps.length == 0 ? EnumSet.noneOf(Capability.class) : EnumSet.copyOf(List.of(caps)); } + this.set = caps.length == 0 ? CapabilitySet.none() : CapabilitySet.from(caps); } public static Optional<Predefined> fromName(String name) { return Arrays.stream(values()).filter(p -> p.name.equals(name)).findAny(); } + + public CapabilitySet capabilities() { return set; } } private static final CapabilitySet ALL_CAPABILITIES = new CapabilitySet(EnumSet.allOf(Capability.class)); @@ -52,7 +54,7 @@ public class CapabilitySet { EnumSet<Capability> caps = EnumSet.noneOf(Capability.class); for (String name : names) { Predefined predefined = Predefined.fromName(name).orElse(null); - if (predefined != null) caps.addAll(predefined.caps); + if (predefined != null) caps.addAll(predefined.set.caps); else caps.add(Capability.fromName(name)); } return new CapabilitySet(caps); diff --git a/security-utils/src/main/java/com/yahoo/security/tls/ConnectionAuthContext.java b/security-utils/src/main/java/com/yahoo/security/tls/ConnectionAuthContext.java index b4e8878fb01..f231e8429ce 100644 --- a/security-utils/src/main/java/com/yahoo/security/tls/ConnectionAuthContext.java +++ b/security-utils/src/main/java/com/yahoo/security/tls/ConnectionAuthContext.java @@ -7,28 +7,78 @@ import java.security.cert.X509Certificate; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.logging.Logger; import static com.yahoo.security.SubjectAlternativeName.Type.DNS; import static com.yahoo.security.SubjectAlternativeName.Type.URI; +import static com.yahoo.security.tls.CapabilityMode.DISABLE; +import static com.yahoo.security.tls.CapabilityMode.LOG_ONLY; /** * @author bjorncs */ public record ConnectionAuthContext(List<X509Certificate> peerCertificateChain, CapabilitySet capabilities, - Set<String> matchedPolicies) { + Set<String> matchedPolicies, + CapabilityMode capabilityMode) { - private static final ConnectionAuthContext DEFAULT_ALL_CAPABILITIES = new ConnectionAuthContext(List.of()); + private static final Logger log = Logger.getLogger(ConnectionAuthContext.class.getName()); public ConnectionAuthContext { peerCertificateChain = List.copyOf(peerCertificateChain); matchedPolicies = Set.copyOf(matchedPolicies); } - private ConnectionAuthContext(List<X509Certificate> certs) { this(certs, CapabilitySet.all(), Set.of()); } + private ConnectionAuthContext(List<X509Certificate> certs, CapabilityMode capabilityMode) { + this(certs, CapabilitySet.all(), Set.of(), capabilityMode); + } public boolean authorized() { return !capabilities.hasNone(); } + /** Throws checked exception to force caller to handle verification failed. */ + public void verifyCapabilities(CapabilitySet requiredCapabilities) throws MissingCapabilitiesException { + verifyCapabilities(requiredCapabilities, null, null, null); + } + + /** + * Throws checked exception to force caller to handle verification failed. + * Provided strings are used for improved logging only + * */ + public void verifyCapabilities(CapabilitySet requiredCapabilities, String action, String resource, String peer) + throws MissingCapabilitiesException { + if (capabilityMode == DISABLE) return; + boolean hasCapabilities = capabilities.has(requiredCapabilities); + if (!hasCapabilities) { + String msg = createPermissionDeniedErrorMessage(requiredCapabilities, action, resource, peer); + if (capabilityMode == LOG_ONLY) { + log.info(msg); + } else { + // Ideally log as warning, but we have no mechanism for de-duplicating repeated log spamming. + log.fine(msg); + throw new MissingCapabilitiesException(msg); + } + } + } + + String createPermissionDeniedErrorMessage( + CapabilitySet required, String action, String resource, String peer) { + StringBuilder b = new StringBuilder(); + if (capabilityMode == LOG_ONLY) b.append("Dry-run: "); + b.append("Permission denied"); + if (resource != null) { + b.append(" for '"); + if (action != null) { + b.append(action).append("' on '"); + } + b.append(resource).append("'"); + } + b.append(". Peer "); + if (peer != null) b.append("'").append(peer).append("' "); + return b.append("with ").append(peerCertificateString().orElse("<missing-certificate>")).append(". Requires capabilities ") + .append(required.toNames()).append(" but peer has ").append(capabilities.toNames()) + .append(".").toString(); + } + public Optional<X509Certificate> peerCertificate() { return peerCertificateChain.isEmpty() ? Optional.empty() : Optional.of(peerCertificateChain.get(0)); } @@ -62,11 +112,11 @@ public record ConnectionAuthContext(List<X509Certificate> peerCertificateChain, } /** Construct instance with all capabilities */ - public static ConnectionAuthContext defaultAllCapabilities() { return DEFAULT_ALL_CAPABILITIES; } + public static ConnectionAuthContext defaultAllCapabilities() { return new ConnectionAuthContext(List.of(), DISABLE); } /** Construct instance with all capabilities */ public static ConnectionAuthContext defaultAllCapabilities(List<X509Certificate> certs) { - return new ConnectionAuthContext(certs); + return new ConnectionAuthContext(certs, DISABLE); } } diff --git a/security-utils/src/main/java/com/yahoo/security/tls/MissingCapabilitiesException.java b/security-utils/src/main/java/com/yahoo/security/tls/MissingCapabilitiesException.java new file mode 100644 index 00000000000..1c3ad9444e4 --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/MissingCapabilitiesException.java @@ -0,0 +1,13 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security.tls; + +/** + * Intentionally checked to force caller to handle missing permissions at call site. + * + * @author bjorncs + */ +public class MissingCapabilitiesException extends Exception { + + public MissingCapabilitiesException(String message) { super(message); } + +} diff --git a/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizationFailedException.java b/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizationFailedException.java new file mode 100644 index 00000000000..02dbf3bb8e7 --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizationFailedException.java @@ -0,0 +1,23 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security.tls; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * @author bjorncs + */ +public class PeerAuthorizationFailedException extends CertificateException { + private final List<X509Certificate> certChain; + + public PeerAuthorizationFailedException(String msg, List<X509Certificate> certChain) { + super(msg); + this.certChain = certChain; + } + + public PeerAuthorizationFailedException(String msg) { this(msg, List.of()); } + + public List<X509Certificate> peerCertificateChain() { return certChain; } +} + diff --git a/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizer.java b/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizer.java index 5db86fd93bc..951b5c57c9e 100644 --- a/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizer.java +++ b/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizer.java @@ -7,7 +7,6 @@ import com.yahoo.security.X509CertificateUtils; import java.security.cert.X509Certificate; import java.util.HashSet; import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.logging.Logger; @@ -39,7 +38,7 @@ public class PeerAuthorizer { X509Certificate cert = certChain.get(0); Set<String> matchedPolicies = new HashSet<>(); Set<CapabilitySet> grantedCapabilities = new HashSet<>(); - String cn = getCommonName(cert).orElse(null); + String cn = X509CertificateUtils.getSubjectCommonName(cert).orElse(null); List<String> sans = getSubjectAlternativeNames(cert); log.fine(() -> String.format("Subject info from x509 certificate: CN=[%s], 'SAN=%s", cn, sans)); for (PeerPolicy peerPolicy : authorizedPeers.peerPolicies()) { @@ -48,7 +47,10 @@ public class PeerAuthorizer { grantedCapabilities.add(peerPolicy.capabilities()); } } - return new ConnectionAuthContext(certChain, CapabilitySet.unionOf(grantedCapabilities), matchedPolicies); + // TODO Pass this through constructor + CapabilityMode capabilityMode = TransportSecurityUtils.getCapabilityMode(); + return new ConnectionAuthContext( + certChain, CapabilitySet.unionOf(grantedCapabilities), matchedPolicies, capabilityMode); } private static boolean matchesPolicy(PeerPolicy peerPolicy, String cn, List<String> sans) { @@ -69,11 +71,6 @@ public class PeerAuthorizer { } } - private static Optional<String> getCommonName(X509Certificate peerCertificate) { - return X509CertificateUtils.getSubjectCommonNames(peerCertificate).stream() - .findFirst(); - } - private static List<String> getSubjectAlternativeNames(X509Certificate peerCertificate) { return X509CertificateUtils.getSubjectAlternativeNames(peerCertificate).stream() .filter(san -> san.getType() == DNS || san.getType() == IP || san.getType() == URI) diff --git a/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizerTrustManager.java b/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizerTrustManager.java index b92cd6c9538..c3dcdf1dc9e 100644 --- a/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizerTrustManager.java +++ b/security-utils/src/main/java/com/yahoo/security/tls/PeerAuthorizerTrustManager.java @@ -6,6 +6,7 @@ import com.yahoo.security.X509CertificateUtils; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; import javax.net.ssl.X509ExtendedTrustManager; import java.net.Socket; @@ -13,18 +14,20 @@ import java.security.KeyStore; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.List; -import java.util.Optional; +import java.util.logging.Level; import java.util.logging.Logger; /** * A {@link X509ExtendedTrustManager} that performs additional certificate verification through {@link PeerAuthorizer}. * + * Implementation assumes that provided {@link X509ExtendedTrustManager} will throw {@link IllegalArgumentException} + * when chain is empty or null. + * * @author bjorncs */ -// Note: Implementation assumes that provided X509ExtendedTrustManager will throw IllegalArgumentException when chain is empty or null -public class PeerAuthorizerTrustManager extends X509ExtendedTrustManager { +class PeerAuthorizerTrustManager extends X509ExtendedTrustManager { - public static final String HANDSHAKE_SESSION_AUTH_CONTEXT_PROPERTY = "vespa.tls.auth.ctx"; + static final String AUTH_CONTEXT_PROPERTY = "vespa.tls.auth.ctx"; private static final Logger log = Logger.getLogger(PeerAuthorizerTrustManager.class.getName()); @@ -33,20 +36,16 @@ public class PeerAuthorizerTrustManager extends X509ExtendedTrustManager { private final AuthorizationMode mode; private final HostnameVerification hostnameVerification; - public PeerAuthorizerTrustManager(AuthorizedPeers authorizedPeers, - AuthorizationMode mode, - HostnameVerification hostnameVerification, - X509ExtendedTrustManager defaultTrustManager) { + PeerAuthorizerTrustManager(AuthorizedPeers authorizedPeers, AuthorizationMode mode, + HostnameVerification hostnameVerification, X509ExtendedTrustManager defaultTrustManager) { this.authorizer = new PeerAuthorizer(authorizedPeers); this.mode = mode; this.hostnameVerification = hostnameVerification; this.defaultTrustManager = defaultTrustManager; } - public PeerAuthorizerTrustManager(AuthorizedPeers authorizedPeers, - AuthorizationMode mode, - HostnameVerification hostnameVerification, - KeyStore truststore) { + PeerAuthorizerTrustManager(AuthorizedPeers authorizedPeers, AuthorizationMode mode, + HostnameVerification hostnameVerification, KeyStore truststore) { this(authorizedPeers, mode, hostnameVerification, TrustManagerUtils.createDefaultX509TrustManager(truststore)); } @@ -65,27 +64,27 @@ public class PeerAuthorizerTrustManager extends X509ExtendedTrustManager { @Override public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { defaultTrustManager.checkClientTrusted(chain, authType, socket); - authorizePeer(chain, authType, true, null); + authorizePeer(chain, authType, true, ((SSLSocket)socket).getHandshakeSession()); } @Override public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { overrideHostnameVerificationForClient(socket); defaultTrustManager.checkServerTrusted(chain, authType, socket); - authorizePeer(chain, authType, false, null); + authorizePeer(chain, authType, false, ((SSLSocket)socket).getHandshakeSession()); } @Override public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException { defaultTrustManager.checkClientTrusted(chain, authType, sslEngine); - authorizePeer(chain, authType, true, sslEngine); + authorizePeer(chain, authType, true, sslEngine.getHandshakeSession()); } @Override public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException { overrideHostnameVerificationForClient(sslEngine); defaultTrustManager.checkServerTrusted(chain, authType, sslEngine); - authorizePeer(chain, authType, false, sslEngine); + authorizePeer(chain, authType, false, sslEngine.getHandshakeSession()); } @Override @@ -93,21 +92,17 @@ public class PeerAuthorizerTrustManager extends X509ExtendedTrustManager { return defaultTrustManager.getAcceptedIssuers(); } - /** - * Note: The authorization result is only available during handshake. The underlying handshake session is removed once handshake is complete. - */ - public static Optional<ConnectionAuthContext> getConnectionAuthContext(SSLEngine sslEngine) { - return Optional.ofNullable(sslEngine.getHandshakeSession()) - .flatMap(session -> Optional.ofNullable((ConnectionAuthContext) session.getValue(HANDSHAKE_SESSION_AUTH_CONTEXT_PROPERTY))); - } - - private void authorizePeer(X509Certificate[] certChain, String authType, boolean isVerifyingClient, SSLEngine sslEngine) throws CertificateException { + private void authorizePeer(X509Certificate[] certChain, String authType, boolean isVerifyingClient, + SSLSession handshakeSessionOrNull) throws PeerAuthorizationFailedException { log.fine(() -> "Verifying certificate: " + createInfoString(certChain[0], authType, isVerifyingClient)); ConnectionAuthContext result = mode != AuthorizationMode.DISABLE ? authorizer.authorizePeer(List.of(certChain)) : ConnectionAuthContext.defaultAllCapabilities(List.of(certChain)); - if (sslEngine != null) { // getHandshakeSession() will never return null in this context - sslEngine.getHandshakeSession().putValue(HANDSHAKE_SESSION_AUTH_CONTEXT_PROPERTY, result); + if (handshakeSessionOrNull != null) { + handshakeSessionOrNull.putValue(AUTH_CONTEXT_PROPERTY, result); + } else { + log.log(Level.FINE, + () -> "Warning: unable to provide ConnectionAuthContext as no SSLSession is available"); } if (result.authorized()) { log.fine(() -> String.format("Verification result: %s", result)); @@ -115,7 +110,7 @@ public class PeerAuthorizerTrustManager extends X509ExtendedTrustManager { String errorMessage = "Authorization failed: " + createInfoString(certChain[0], authType, isVerifyingClient); log.warning(errorMessage); if (mode == AuthorizationMode.ENFORCE) { - throw new CertificateException(errorMessage); + throw new PeerAuthorizationFailedException(errorMessage, List.of(certChain)); } } } diff --git a/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityUtils.java b/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityUtils.java index 21d97613f95..ae6cef65156 100644 --- a/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityUtils.java +++ b/security-utils/src/main/java/com/yahoo/security/tls/TransportSecurityUtils.java @@ -1,6 +1,9 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.security.tls; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; @@ -87,6 +90,24 @@ public class TransportSecurityUtils { } } + /** + * @return {@link ConnectionAuthContext} instance if {@link SSLEngine} was constructed by a {@link TlsContext}. + * Only available after TLS handshake is completed. + */ + public static Optional<ConnectionAuthContext> getConnectionAuthContext(SSLSession s) { + return Optional.ofNullable((ConnectionAuthContext) s.getValue(PeerAuthorizerTrustManager.AUTH_CONTEXT_PROPERTY)); + } + + /** @see #getConnectionAuthContext(SSLSession) */ + public static Optional<ConnectionAuthContext> getConnectionAuthContext(SSLEngine e) { + return getConnectionAuthContext(e.getSession()); + } + + /** @see #getConnectionAuthContext(SSLSession) */ + public static Optional<ConnectionAuthContext> getConnectionAuthContext(SSLSocket s) { + return getConnectionAuthContext(s.getSession()); + } + private static Optional<String> getEnvironmentVariable(Map<String, String> environmentVariables, String variableName) { return Optional.ofNullable(environmentVariables.get(variableName)) .filter(var -> !var.isEmpty()); diff --git a/security-utils/src/test/java/com/yahoo/security/tls/ConnectionAuthContextTest.java b/security-utils/src/test/java/com/yahoo/security/tls/ConnectionAuthContextTest.java new file mode 100644 index 00000000000..c30a812a30d --- /dev/null +++ b/security-utils/src/test/java/com/yahoo/security/tls/ConnectionAuthContextTest.java @@ -0,0 +1,62 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security.tls; + +import com.yahoo.security.KeyAlgorithm; +import com.yahoo.security.KeyUtils; +import com.yahoo.security.X509CertificateBuilder; +import org.junit.jupiter.api.Test; + +import javax.security.auth.x500.X500Principal; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Set; + +import static com.yahoo.security.SignatureAlgorithm.SHA256_WITH_ECDSA; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * @author bjorncs + */ +class ConnectionAuthContextTest { + + @Test + void fails_on_missing_capabilities() { + ConnectionAuthContext ctx = createConnectionAuthContext(); + assertThrows(MissingCapabilitiesException.class, + () -> ctx.verifyCapabilities(CapabilitySet.from(Capability.CONTENT__STATUS_PAGES))); + } + + @Test + void creates_correct_error_message() { + ConnectionAuthContext ctx = createConnectionAuthContext(); + CapabilitySet requiredCaps = CapabilitySet.from(Capability.CONTENT__STATUS_PAGES); + String expectedMessage = """ + Permission denied for 'myaction' on 'myresource'. Peer 'mypeer' with [CN='myidentity']. + Requires capabilities [vespa.content.status_pages] but peer has + [vespa.content.document_api, vespa.content.search_api, vespa.slobrok.api]. + """; + String actualMessage = ctx.createPermissionDeniedErrorMessage(requiredCaps, "myaction", "myresource", "mypeer"); + assertThat(actualMessage).isEqualToIgnoringWhitespace(expectedMessage); + } + + private static ConnectionAuthContext createConnectionAuthContext() { + return new ConnectionAuthContext( + List.of(createCertificate()), CapabilitySet.Predefined.CONTAINER_NODE.capabilities(), Set.of(), + CapabilityMode.ENFORCE); + } + + private static X509Certificate createCertificate() { + KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256); + return X509CertificateBuilder.fromKeypair( + keyPair, new X500Principal("CN=myidentity"), Instant.EPOCH, + Instant.EPOCH.plus(100000, ChronoUnit.DAYS), SHA256_WITH_ECDSA, BigInteger.ONE) + .build(); + } + + +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/AthenzX509CertificateUtils.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/AthenzX509CertificateUtils.java index 7542e976260..9d47ce79f87 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/AthenzX509CertificateUtils.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/tls/AthenzX509CertificateUtils.java @@ -50,7 +50,7 @@ public class AthenzX509CertificateUtils { } public static AthenzRole getRolesFromRoleCertificate(X509Certificate certificate) { - String commonName = com.yahoo.security.X509CertificateUtils.getSubjectCommonNames(certificate).get(0); + String commonName = X509CertificateUtils.getSubjectCommonName(certificate).orElseThrow(); return AthenzRole.fromResourceNameString(commonName); } diff --git a/vespamalloc/CMakeLists.txt b/vespamalloc/CMakeLists.txt index df8e38653bb..af71d8b7d82 100644 --- a/vespamalloc/CMakeLists.txt +++ b/vespamalloc/CMakeLists.txt @@ -26,7 +26,6 @@ vespa_define_module( src/vespamalloc/util ) -vespa_install_script(bin/parsememorydump.pl vespa-malloc-parse-memorydump.pl bin) else() install(DIRECTORY DESTINATION lib64/vespa) endif() diff --git a/vespamalloc/bin/parsememorydump.pl b/vespamalloc/bin/parsememorydump.pl deleted file mode 100755 index 95c70859b9a..00000000000 --- a/vespamalloc/bin/parsememorydump.pl +++ /dev/null @@ -1,314 +0,0 @@ -#!/usr/bin/perl -w -# Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. - -# This is a tool to parse the output given by vespamalloc when dumping all -# memory usage with stacktraces. -# -# This tool will try to group similar stack traces together, identify them, -# and show a report of how much memory you use for various tasks in the given -# dump. -# -# Suggested usage: -# - Manually rotate your vespa.log file. -# - Send signal to vespamalloc to make it dump stack traces. -# - When dump is done, cat all the log files generated in correct order into -# one file. -# - Send this file to STDIN of this script. -# -# By doing it like this, it's easy to rerun this script and to update it to -# work with anything encountered in your report that this script doesn't -# currently handle. -# -# It is for instance likely, that you need to add some more regexes to match -# unknown stack traces into appropriate groups. - -use strict; - -# If this variable is set, unrecognized stack traces will be concatenated to -# one unrecognized chunk. -my $combineUnrecognizedStacks = 1; -# If this variable is set, all stack traces are shown, no matter if they are -# recognized or not. -my $showRecognizedStacks = 0; -# If this variable is set, memory usage is grouped on the first word in the -# group name. Thus, by using a proper name convention, one can distinguish -# how much memory are used on various parts of the system -my $sortOnCategories = 1; - -# These patterns are used to recognized what memory is used for based on the -# stacktrace of the allocation. The keys are regular expressions that can match -# stacktraces. The value is the group name this allocation will be marked as if -# the trace matches the regex. If matching multiple entries, the first entry -# found will be the match. - -# Note that as I made this tool to analyze VDS memory usage, the stuff added is -# just stuff I found using memory in that current run. We should probably add -# more patterns as needed. -my %patterns = ( - - 'FRT_MemoryTub::BigAlloc.*mbus::MessageBus::send.*' => 'RPC send messagebus message - big alloc', - 'FRT_MemoryTub::BigAlloc.*storage::rpc::Destination::sendRequest', => 'RPC send storage API command - big alloc', - 'FRT_RPCRequestPool::AllocRPCRequest.*storage::rpc::Destination::sendRequest' => 'RPC send storage API command', - - 'FNET_DataBuffer::Shrink.*FNET_Connection::HandleWriteEvent' => 'RPC shrinked write buffer', - 'FNET_DataBuffer::Shrink.*FNET_Connection::HandleReadEvent' => 'RPC shrinked read buffer', - - 'FRT_MemoryTub::BigAlloc.*FRT_RPCRequestPacket::Decode.*FNET_Connection::HandleReadEvent' => 'RPC read event - decode - big alloc', - 'FRT_RPCRequestPool::AllocRPCRequest.*FNET_Connection::HandlePacket.*FNET_Connection::HandleReadEvent' => 'RPC read event - handle packet', - 'FNET_DataBuffer::Pack.*FNET_Connection::HandleReadEvent' => 'RPC packed read buffer', - 'FNET_DataBuffer::Pack.*FNET_Connection::HandleWriteEvent' => 'RPC packed write buffer', - 'FNET_PacketQueue_NoLock::ExpandBuf.*FNET_Connection::HandleWriteEvent()' => 'RPC expand write buffer', - 'FNET_ChannelPool::AllocChannelCluster.*storage::rpc::Destination::sendRequest' => 'RPC storage API alloc channel cluster', - - 'storage::SlotFileBuffer::getAlignedBuffer.*SlotFileImpl::close' => 'Persistence layer - move buffer to cache', - 'storage::SlotFileBuffer::getAlignedBuffer.*SlotFileBuffer::getBuffer' => 'Persistence layer - Get aligned buffer', - 'storage::SlotFileBuffer::getInputBuffer' => 'Persistence layer - Input buffer', - 'storage::SlotFileBuffer::getIndexBuffer' => 'Persistence layer - Index buffer', - 'storage::SlotFileBuffer::getOutputBuffer' => 'Persistence layer - Output buffer', - - 'storage::api::\S*::makeReply.*storage::MessageDispatcher::handleCommand' => 'Messages - Replies stored in message dispatcher', - 'document::SerializableArray::onDeserialize.*storage::CommunicationManager::onEvent\(std::auto_ptr<storage::rpc::Event>\)' - => 'Messages - Documents from storage API messages - serializable arrays', - 'document::SerializableDocumentSharedPointer.*storage::rpc::Handle::onEvent\(std::auto_ptr<storage::rpc::Event>\)' - => 'Messages - Documents from storage API messages - original byte buffers', - 'storage::api::StorageMessageAddress::create.*storage::rpc::Handle::onEvent\(std::auto_ptr<storage::rpc::Event>\)' - => 'Messages - Storage API message addresses', - 'document::IdString::createIdString.*storage::api::StorageMessage::onDeserialize' - => 'Messages - Document identifiers', - 'storage::api::ApplyBucketDiffCommand::Entry::Entry.*storage::FileStorThread::run' - => 'Messages - Merge apply entries', - 'storage::api::ApplyBucketDiffCommand::Entry::Entry.*storage::api::StorageMessage::onDeserialize' - => 'Messages - Merge apply entries', - => 'Messages - Queued multi operation commands - cloned for local use in message dispatcher', - - 'storage::BufHolder::reserve.*storage::FileStorThread::onGetIterCommand' => 'Visiting - Downsized docblocks', - 'storage::BufHolder::resize.*storage::FileStorThread::onGetIterCommand' => 'Visiting - Upsized docblocks', - - 'JudyLIns.*storage::StorBucketDatabase::(get|insert)' => 'Bucket database - Judy', - 'JudyLDel.*WrappedEntry::remove' => 'Bucket database - Judy', - 'std::vector<.*storage::LockableMap.*::WrappedEntry::remove()' => 'Bucket database - Lockable map lock list', - - 'document::Printable::toString' - => 'Other - Temporary data created during toString operations', - - 'metrics::MetricsManager::getSnapshotForConsumer' => 'Metrics - Snapshot created', - 'storage::BucketManagerMetrics::BucketManagerMetrics\(\)' => 'Metrics - Stored metrics', - 'storage::FileStorThreadMetrics::FileStorThreadMetrics\(std::basic_string' => 'Metrics - Stored metrics', - 'metrics::MetricsSet::addAll' => 'Metrics - Add all', - 'storage::StatusMetricConsumer::run\(\)' => 'Metrics', - - 'slobrok::api::MirrorAPI::PerformTask\(\)' => 'Slobrok - Mirror API perform task' - -); - -my $signal; -my $starttime; -my $size; -my %vals; -my %counts; - -my $mallocfault; -my $local = 0; -my $global = 0; -my $globwaste = 0; - -# Go through all the input gotten on STDIN. -foreach (<>) { - if (/^(\d+)\.\d+\s+\S+\s+\d+\s+\S+\s+\S+\s+warning\s+(.*)$/) { - # We only care for warnings to logs printed by vespamalloc - my ($time, $line) = ($1, $2); - if ($line =~ /SignalHandler (\d+) caught/) { - # If a dump is just starting, reset all the variables that keeps - # state - $signal = $1; - $size = 0; - %vals = (); - %counts = (); - $local = 0; - $global = 0; - $globwaste = 0; - $mallocfault = undef; - $starttime = $time; - print "\nProcessing dump from " . localtime($starttime) . "\n"; - } elsif ($line =~ /^SignalHandler $signal done/) { - # If we are at the end of the vespamalloc report, print a report of - # what we have found. - &printReport(); - $starttime = undef; - } elsif (!$starttime) { - # Ignore output that is not within dump - } elsif ($line =~ /^SC\s*\d+\(\s*(\d+)\)\s*GetAlloc\(\s*(\d+)\)\s*GetFree\(\s*\d+\)\s*ExChangeAlloc\(\s*(\d+)\)\s*ExChangeFree\(\s*(\d+)\)\s*ExactAlloc\(\s*(\d+)\)\s*Returned\(\s*(\d+)\)\s*Malloc\(\s*(\d+)\)\s*/) { - # Track size groups allocated in hopes of figuring out how much data - # waste there is in vespa malloc - my ($size, $get, $exalloc, $exfree, $exact, $returned, $malloc) = ($1, $2, $3, $4, $5, $6, $7); - #print "$line\n"; - #print "Size $size, GetAlloc $get, ExChangeAlloc $exalloc, ExChangeFree $exfree, ExactAlloc $exact, Returned $returned, Malloc $malloc\n"; - my $mult = 65536; - if ($size > $mult) { $mult = $size; } - my $alloced = $get + $exalloc + $exact; - my $ret = $exfree + $returned; - if ($size != 2097152) { - #print "Adding ($alloced - $ret) * $mult = " . (($alloced - $ret) * $mult) . "\n"; - my $global = ($alloced - $ret) * $mult; - my $waste = $malloc * 1024 * 1024 - ($alloced - $ret) * $mult; - #print "Size $size - Global $global - Waste $waste\n"; - #$global += ($alloced - $ret) * $mult; - if ($waste >= 0) { - $global += $global; - $globwaste += $waste; - } else { - $mallocfault = 1; - } - } else { - $mallocfault = 1; - } - } elsif ($line =~ /SC\s*\d+\(\s*(\d+)\)\s*Local\(\s*(\d+)\)/) { - # Track size groups allocated in hopes of figuring out how much data - # waste there is in vespa malloc - - #print "Local $1 * $2 = ".($1 * $2)."\n"; - $local += $1 * $2; - } elsif ($line =~ /^SizeClass\s*(\d+)/) { - #print "$line\n"; - } elsif ($line =~ /^(Usage)/) { - print "Vespa Malloc $line\n"; - } elsif ($line =~ /(DataSegment|Free|Start)/) { - } elsif ($line =~ /^(\d+)\s+:\s+\{\s+\S+\s+\S+\s+(\S+[^\}]*)/) { - # This should match any stacktrace reported. Add this stacktrace - # to the report. - my ($count, $value) = ($1, $2); - # Unify stack trace - $value =~ s/0x[0-9a-f]+//g; - $value =~ s/\(\d+\)//g; - $value =~ s/\(\)//g; - # Remove multiple UNKNOWN lines in backtrace - my @stack = split /\s+/, $value; - $value = ''; - my $last = ""; - foreach (@stack) { - my $current = &cppfilt($_); - if ($last !~ /UNKNOWN/ || $current !~ /UNKNOWN/) { # Don't print multiple unknown entries after one another - $value .= "\n " . &cppfilt($_); - } - $last = $current; - } - # Detect known stack traces - - #print "$count - $value\n"; - my $replaced = 0; - foreach my $pat (keys %patterns) { - #print "Does $value match $pat?\n"; - if ($value =~ /$pat/s) { - if ($showRecognizedStacks) { - $value = $patterns{$pat} . $value; - } else { - $value = $patterns{$pat}; - } - $replaced = 1; - last; - } - } - if (!$replaced) { - if ($combineUnrecognizedStacks) { - $value = "Unrecognized memory allocations"; - } elsif ($sortOnCategories) { - $value = "Unrecognized memory allocations" . $value; - } - } - if (exists $vals{$value}) { - $vals{$value} += $count * $size; - $counts{$value} += $count; - } else { - $vals{$value} = $count * $size; - $counts{$value} = $count; - } - } elsif ($line =~ /^Allocated Blocks SC\s*\d+\(\s*(\d+)/) { - $size = $1; - #print "Size: $1\n"; - } else { - #print "$line\n"; - } - } else { - #print "$_"; - } -} -if ($starttime) { - print "Input stopped in incomplete trace.\n"; -} - -exit(0); - -my %filtered; - -sub cppfilt { - my $val = $_[0]; - if ($val =~ /UNKNOWN/) { - return "UNKNOWN"; - } elsif (exists $filtered{$val}) { - return $filtered{$val}; - } else { - my $result = `c++filt $val`; - chomp $result; - $filtered{$val} = $result; - return $result; - } -} - -sub getByteString { - my $val = $_[0]; - if ($val < 5000) { - return sprintf("%d B", $val); - } elsif ($val < 5000000) { - return sprintf("%d kB", $val / 1024); - } else { - return sprintf("%d MB", $val / (1024 * 1024)); - } -} - -sub printReport { - print "Total vespa malloc unused thread local data: " . &getByteString($local) . "\n"; - #print "Total global data: $global\n"; - print "Total vespa malloc global waste data: " . &getByteString($globwaste). "\n"; - - my $total = 0; - foreach (keys %vals) { - $total += $vals{$_}; - } - if (defined $mallocfault) { - print "Warning: Some sketchy numbers from vespa malloc, so global and thread local " - . "data is probably inaccurate.\n"; - } - - print "\nSummary of allocated data:\n"; - print &getByteString($total) . " bytes tracked total\n"; - - if ($sortOnCategories) { - my %categories; - foreach (keys %vals) { - if (/^\s*(\S+)/) { - my $category = $1; - if (exists $categories{$category}) { - ${$categories{$category}}[0] += $vals{$_}; - push @{$categories{$category}}, $_; - } else { - $categories{$category} = [ $vals{$_}, $_ ]; - } - } - } - foreach (sort { ${$categories{$b}}[0] <=> ${$categories{$a}}[0] } keys %categories) { - my @values = @{ $categories{$_} }; - print &getByteString($values[0]) . " - $_\n"; - shift @values; - - foreach my $val (sort { $vals{$b} <=> $vals{$a} } @values) { - my $stack = $val; - $stack =~ s/\n/\n /sg; - print " " . &getByteString($vals{$val}) . " - " . $counts{$val} . " allocations - $stack\n"; - } - } - } else { - foreach (sort { $vals{$b} <=> $vals{$a} } keys %vals) { - print &getByteString($vals{$_}) . " - " . $counts{$_} . " allocations - $_\n"; - } - } -} |