Skip to content

Commit 55b881e

Browse files
committed
Adds support for configuring CRL or custom blacklisting on tomcat SSL
1 parent c956378 commit 55b881e

9 files changed

+552
-1
lines changed

kork-web/kork-web.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ dependencies {
77
compile spinnaker.dependency('groovy')
88
compile spinnaker.dependency('okHttp')
99
compile spinnaker.dependency('retrofit')
10+
compile spinnaker.dependency('guava')
1011

1112

1213
spinnaker.group('spockBase')

kork-web/src/main/groovy/com/netflix/spinnaker/config/TomcatConfiguration.groovy

+27-1
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,17 @@
1616

1717
package com.netflix.spinnaker.config
1818

19+
import com.netflix.spinnaker.tomcat.x509.BlacklistingSSLImplementation
1920
import groovy.util.logging.Slf4j
2021
import org.apache.catalina.connector.Connector
22+
import org.apache.coyote.http11.AbstractHttp11JsseProtocol
2123
import org.apache.coyote.http11.Http11NioProtocol
2224
import org.springframework.beans.factory.annotation.Value
2325
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression
2426
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer
2527
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer
2628
import org.springframework.boot.context.embedded.Ssl
29+
import org.springframework.boot.context.embedded.tomcat.TomcatConnectorCustomizer
2730
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory
2831
import org.springframework.context.annotation.Bean
2932
import org.springframework.context.annotation.Configuration
@@ -37,6 +40,12 @@ class TomcatConfiguration {
3740
@Value('${default.apiPort:-1}')
3841
int apiPort
3942

43+
//TODO(cfieber) remove this when https://github.com/spring-projects/spring-boot/issues/6171 is implemented
44+
// the default value of null equates to no CRL file in the connector config so we don't have to null-check it
45+
// below
46+
@Value('${server.ssl.crlFile:#{null}}')
47+
String crlFile
48+
4049
/**
4150
* Setup multiple connectors:
4251
* - an https connector requiring client auth that will service API requests
@@ -47,6 +56,19 @@ class TomcatConfiguration {
4756
EmbeddedServletContainerCustomizer containerCustomizer() throws Exception {
4857
return { ConfigurableEmbeddedServletContainer container ->
4958
TomcatEmbeddedServletContainerFactory tomcat = (TomcatEmbeddedServletContainerFactory) container
59+
//this will only handle the case where SSL is enabled on the main tomcat connector
60+
tomcat.addConnectorCustomizers(new TomcatConnectorCustomizer() {
61+
@Override
62+
void customize(Connector connector) {
63+
def handler = connector.getProtocolHandler()
64+
if (handler instanceof AbstractHttp11JsseProtocol) {
65+
if (handler.isSSLEnabled()) {
66+
handler.setSslImplementationName(BlacklistingSSLImplementation.name)
67+
handler.setCrlFile(crlFile)
68+
}
69+
}
70+
}
71+
})
5072

5173
if (legacyServerPort > 0) {
5274
log.info("Creating legacy connector on port ${legacyServerPort}")
@@ -70,7 +92,11 @@ class TomcatConfiguration {
7092
}
7193
ssl.clientAuth = Ssl.ClientAuth.NEED
7294

73-
tomcat.configureSsl(apiConnector.getProtocolHandler() as Http11NioProtocol, ssl)
95+
Http11NioProtocol handler = apiConnector.getProtocolHandler() as Http11NioProtocol
96+
handler.setCrlFile(crlFile)
97+
handler.setSslImplementationName(BlacklistingSSLImplementation.name)
98+
99+
tomcat.configureSsl(handler, ssl)
74100
tomcat.addAdditionalTomcatConnectors(apiConnector)
75101
}
76102
} as EmbeddedServletContainerCustomizer
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2016 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License")
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.netflix.spinnaker.tomcat.x509;
18+
19+
import java.security.cert.X509Certificate;
20+
21+
public interface Blacklist {
22+
23+
static Blacklist forFile(String blacklistFile) {
24+
return new ReloadingFileBlacklist(blacklistFile);
25+
}
26+
27+
boolean isBlacklisted(X509Certificate cert);
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2016 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License")
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.netflix.spinnaker.tomcat.x509;
18+
19+
import org.apache.tomcat.util.net.AbstractEndpoint;
20+
import org.apache.tomcat.util.net.jsse.JSSESocketFactory;
21+
22+
import javax.net.ssl.TrustManager;
23+
import javax.net.ssl.X509TrustManager;
24+
import java.util.Optional;
25+
26+
public class BlacklistingJSSESocketFactory extends JSSESocketFactory {
27+
private static final String BLACKLIST_PREFIX = "blacklist:";
28+
29+
private final Blacklist blacklist;
30+
31+
public BlacklistingJSSESocketFactory(AbstractEndpoint<?> endpoint) {
32+
super(endpoint);
33+
String blacklistFile = Optional.ofNullable(endpoint.getCrlFile())
34+
.filter(file -> file.startsWith(BLACKLIST_PREFIX))
35+
.map(file -> file.substring(BLACKLIST_PREFIX.length()))
36+
.orElse(null);
37+
38+
if (blacklistFile != null) {
39+
endpoint.setCrlFile(null);
40+
blacklist = Blacklist.forFile(blacklistFile);
41+
} else {
42+
blacklist = null;
43+
}
44+
}
45+
46+
@Override
47+
public TrustManager[] getTrustManagers() throws Exception {
48+
TrustManager[] trustManagers = super.getTrustManagers();
49+
if (blacklist != null && trustManagers != null) {
50+
int delegatedCount = 0;
51+
for (int i = 0; i < trustManagers.length; i++) {
52+
TrustManager tm = trustManagers[i];
53+
if (tm instanceof X509TrustManager) {
54+
trustManagers[i] = new BlacklistingX509TrustManager((X509TrustManager) tm, blacklist);
55+
delegatedCount++;
56+
}
57+
}
58+
59+
if (delegatedCount != 1) {
60+
throw new IllegalStateException("expected single X509TrustManager");
61+
}
62+
}
63+
64+
return trustManagers;
65+
}
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2016 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License")
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.netflix.spinnaker.tomcat.x509;
18+
19+
import org.apache.tomcat.util.net.AbstractEndpoint;
20+
import org.apache.tomcat.util.net.SSLUtil;
21+
import org.apache.tomcat.util.net.ServerSocketFactory;
22+
import org.apache.tomcat.util.net.jsse.JSSEImplementation;
23+
24+
/**
25+
* An SSLImplementation that enforces a blacklist of client certificates.
26+
*
27+
* To enable the blacklist behavior, use the {{AbstractEndpoint}} {{crlFile}} property
28+
* following the naming convention {{blacklist:/path/to/blacklist/file}}
29+
*
30+
* The format of the blacklist file is one entry per line conforming to
31+
* {{issuer X500 name:::blacklisted certificate serial number}}
32+
* Blank lines and lines that begin with {{#}} are ignored.
33+
*
34+
* The Blacklist file is refreshed periodically, so it can be updated in place to pick up newly
35+
* revoked certificates.
36+
*/
37+
public class BlacklistingSSLImplementation extends JSSEImplementation {
38+
@Override
39+
public ServerSocketFactory getServerSocketFactory(AbstractEndpoint<?> endpoint) {
40+
return new BlacklistingJSSESocketFactory(endpoint);
41+
}
42+
43+
@Override
44+
public SSLUtil getSSLUtil(AbstractEndpoint<?> endpoint) {
45+
return new BlacklistingJSSESocketFactory(endpoint);
46+
}
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2016 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License")
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.netflix.spinnaker.tomcat.x509;
18+
19+
import javax.net.ssl.X509TrustManager;
20+
import java.security.cert.CRLReason;
21+
import java.security.cert.CertificateException;
22+
import java.security.cert.CertificateRevokedException;
23+
import java.security.cert.X509Certificate;
24+
import java.util.Collections;
25+
import java.util.Date;
26+
import java.util.Objects;
27+
28+
public class BlacklistingX509TrustManager implements X509TrustManager {
29+
private final X509TrustManager delegate;
30+
private final Blacklist blacklist;
31+
32+
public BlacklistingX509TrustManager(X509TrustManager delegate, Blacklist blacklist) {
33+
this.delegate = Objects.requireNonNull(delegate);
34+
this.blacklist = Objects.requireNonNull(blacklist);
35+
}
36+
37+
@Override
38+
public void checkClientTrusted(X509Certificate[] x509Certificates, String authType) throws CertificateException {
39+
if (x509Certificates != null) {
40+
for (X509Certificate cert : x509Certificates) {
41+
if (blacklist.isBlacklisted(cert)) {
42+
throw new CertificateRevokedException(new Date(), CRLReason.UNSPECIFIED, cert.getIssuerX500Principal(), Collections.emptyMap());
43+
}
44+
}
45+
}
46+
47+
delegate.checkClientTrusted(x509Certificates, authType);
48+
}
49+
50+
@Override
51+
public void checkServerTrusted(X509Certificate[] x509Certificates, String authType) throws CertificateException {
52+
delegate.checkServerTrusted(x509Certificates, authType);
53+
}
54+
55+
@Override
56+
public X509Certificate[] getAcceptedIssuers() {
57+
return delegate.getAcceptedIssuers();
58+
}
59+
60+
}
61+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Copyright 2016 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License")
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.netflix.spinnaker.tomcat.x509;
18+
19+
import com.google.common.cache.CacheBuilder;
20+
import com.google.common.cache.CacheLoader;
21+
import com.google.common.cache.LoadingCache;
22+
import com.google.common.collect.ImmutableSet;
23+
24+
import javax.security.auth.x500.X500Principal;
25+
import java.io.File;
26+
import java.math.BigInteger;
27+
import java.nio.file.Files;
28+
import java.security.cert.X509Certificate;
29+
import java.util.Collections;
30+
import java.util.Objects;
31+
import java.util.Set;
32+
import java.util.concurrent.ExecutionException;
33+
import java.util.concurrent.TimeUnit;
34+
import java.util.stream.Collectors;
35+
36+
public class ReloadingFileBlacklist implements Blacklist {
37+
private static class Entry {
38+
private static final String DELIMITER = ":::";
39+
private final X500Principal issuer;
40+
private final BigInteger serial;
41+
42+
private static Entry fromString(String blacklistEntry) {
43+
int idx = blacklistEntry.indexOf(DELIMITER);
44+
if (idx == -1) {
45+
throw new IllegalArgumentException("Missing delimiter " + DELIMITER + " in " + blacklistEntry);
46+
}
47+
X500Principal principal = new X500Principal(blacklistEntry.substring(0, idx));
48+
BigInteger serial = new BigInteger(blacklistEntry.substring(idx + DELIMITER.length()));
49+
return new Entry(principal, serial);
50+
}
51+
52+
private Entry(X500Principal issuer, BigInteger serial) {
53+
this.issuer = Objects.requireNonNull(issuer);
54+
this.serial = Objects.requireNonNull(serial);
55+
}
56+
57+
@Override
58+
public boolean equals(Object o) {
59+
if (this == o) return true;
60+
if (o == null || getClass() != o.getClass()) return false;
61+
62+
Entry entry = (Entry) o;
63+
64+
if (!issuer.equals(entry.issuer)) return false;
65+
return serial.equals(entry.serial);
66+
}
67+
68+
@Override
69+
public int hashCode() {
70+
int result = issuer.hashCode();
71+
result = 31 * result + serial.hashCode();
72+
return result;
73+
}
74+
}
75+
76+
private static final int DEFAULT_RELOAD_INTERVAL_SECONDS = 5;
77+
private final String blacklistFile;
78+
private final LoadingCache<String, Set<Entry>> blacklist;
79+
80+
public ReloadingFileBlacklist(String blacklistFile, long reloadInterval, TimeUnit unit) {
81+
this.blacklistFile = blacklistFile;
82+
this.blacklist = CacheBuilder
83+
.newBuilder()
84+
.expireAfterAccess(reloadInterval, unit)
85+
.build(new CacheLoader<String, Set<Entry>>() {
86+
@Override
87+
public Set<Entry> load(String key) throws Exception {
88+
File f = new File(key);
89+
if (!f.exists()) {
90+
return Collections.emptySet();
91+
}
92+
return ImmutableSet.copyOf(Files.lines(f.toPath())
93+
.map(String::trim)
94+
.filter(line -> !(line.isEmpty() || line.startsWith("#")))
95+
.map(Entry::fromString)
96+
.collect(Collectors.toSet()));
97+
}
98+
});
99+
}
100+
101+
public ReloadingFileBlacklist(String blacklistFile) {
102+
this(blacklistFile, DEFAULT_RELOAD_INTERVAL_SECONDS, TimeUnit.SECONDS);
103+
}
104+
105+
@Override
106+
public boolean isBlacklisted(X509Certificate cert) {
107+
try {
108+
return blacklist.get(blacklistFile).contains(new Entry(cert.getIssuerX500Principal(), cert.getSerialNumber()));
109+
} catch (ExecutionException ee) {
110+
Throwable cause = ee.getCause();
111+
if (cause != null) {
112+
if (cause instanceof RuntimeException) {
113+
throw (RuntimeException) cause;
114+
}
115+
throw new RuntimeException(cause);
116+
}
117+
throw new RuntimeException(ee);
118+
}
119+
120+
}
121+
}

0 commit comments

Comments
 (0)