Skip to content

Commit 31fbe77

Browse files
committed
Start Collector key manager
1 parent 3a5d5cb commit 31fbe77

13 files changed

Lines changed: 690 additions & 52 deletions
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright (C) 2020 Graylog, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the Server Side Public License, version 1,
6+
* as published by MongoDB, Inc.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* Server Side Public License for more details.
12+
*
13+
* You should have received a copy of the Server Side Public License
14+
* along with this program. If not, see
15+
* <http://www.mongodb.com/licensing/server-side-public-license>.
16+
*/
17+
package org.graylog.collectors;
18+
19+
import com.github.benmanes.caffeine.cache.Caffeine;
20+
import com.github.benmanes.caffeine.cache.Expiry;
21+
import com.github.benmanes.caffeine.cache.LoadingCache;
22+
import com.google.common.annotations.VisibleForTesting;
23+
import com.google.common.eventbus.EventBus;
24+
import com.google.common.eventbus.Subscribe;
25+
import com.google.common.util.concurrent.AbstractIdleService;
26+
import jakarta.inject.Inject;
27+
import jakarta.inject.Singleton;
28+
import org.graylog.collectors.events.CollectorCaConfigUpdated;
29+
import org.graylog.security.pki.PemUtils;
30+
import org.graylog2.security.encryption.EncryptedValueService;
31+
import org.slf4j.Logger;
32+
import org.slf4j.LoggerFactory;
33+
34+
import javax.net.ssl.X509KeyManager;
35+
import java.net.Socket;
36+
import java.security.Principal;
37+
import java.security.PrivateKey;
38+
import java.security.cert.X509Certificate;
39+
import java.time.Duration;
40+
41+
@Singleton
42+
public class CollectorCaKeyManager extends AbstractIdleService implements X509KeyManager {
43+
private static final Logger LOG = LoggerFactory.getLogger(CollectorCaKeyManager.class);
44+
private static final String ALIAS = "server";
45+
46+
private final CollectorCaService caService;
47+
private final EncryptedValueService encryptedValueService;
48+
private final EventBus eventBus;
49+
private final LoadingCache<Integer, CacheEntry> cache;
50+
51+
private record CacheEntry(PrivateKey privateKey,
52+
X509Certificate serverCert,
53+
String serverCertFingerprint,
54+
X509Certificate signingCert,
55+
String signingCertFingerprint) {
56+
}
57+
58+
@Inject
59+
public CollectorCaKeyManager(CollectorCaService caService,
60+
EncryptedValueService encryptedValueService,
61+
EventBus eventBus) {
62+
this.caService = caService;
63+
this.encryptedValueService = encryptedValueService;
64+
this.eventBus = eventBus;
65+
this.cache = Caffeine.newBuilder()
66+
.expireAfter(Expiry.<Integer, CacheEntry>creating((key, value) -> Duration.ofSeconds(1)))
67+
.maximumSize(1)
68+
.initialCapacity(1)
69+
.build(this::loadCacheKey);
70+
}
71+
72+
private CacheEntry loadCacheKey(Integer key) {
73+
try {
74+
final var signingCertEntry = caService.getSigningCert();
75+
final var serverCertEntry = caService.getOtlpServerCert();
76+
final var signingCert = PemUtils.parseCertificate(signingCertEntry.certificate());
77+
final var serverCert = PemUtils.parseCertificate(serverCertEntry.certificate());
78+
final var privateKey = PemUtils.parsePrivateKey(encryptedValueService.decrypt(serverCertEntry.privateKey()));
79+
LOG.warn("Loaded server cert <{}> and signing cert <{}>", serverCertEntry.fingerprint(), signingCertEntry.fingerprint());
80+
return new CacheEntry(privateKey, serverCert, serverCertEntry.fingerprint(), signingCert, signingCertEntry.fingerprint());
81+
} catch (Exception e) {
82+
LOG.error("Couldn't load certificates", e);
83+
throw new RuntimeException(e);
84+
}
85+
}
86+
87+
@Override
88+
protected void startUp() throws Exception {
89+
eventBus.register(this);
90+
}
91+
92+
@Override
93+
protected void shutDown() throws Exception {
94+
eventBus.unregister(this);
95+
}
96+
97+
@Subscribe
98+
@VisibleForTesting
99+
void handleCollectorsConfigEvent(CollectorCaConfigUpdated event) {
100+
cache.invalidateAll();
101+
}
102+
103+
@Override
104+
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
105+
if ("EdDSA".equals(keyType)) {
106+
LOG.warn("Returning <{}> as the server alias for key type <{}>", ALIAS, keyType);
107+
return ALIAS;
108+
}
109+
LOG.warn("Returning null for key type <{}>", keyType);
110+
return null;
111+
}
112+
113+
@Override
114+
public X509Certificate[] getCertificateChain(String alias) {
115+
if (ALIAS.equals(alias)) {
116+
final var entry = cache.get(0);
117+
LOG.warn("Returning certificate chain for alias <{}>: server-cert={} signing-cert={}",
118+
alias, entry.serverCertFingerprint(), entry.signingCertFingerprint());
119+
return new X509Certificate[]{entry.serverCert(), entry.signingCert()};
120+
}
121+
LOG.warn("Returning null certificate chain for alias <{}>", alias);
122+
return null;
123+
}
124+
125+
@Override
126+
public PrivateKey getPrivateKey(String alias) {
127+
if (ALIAS.equals(alias)) {
128+
final var entry = cache.get(0);
129+
LOG.warn("Returning private key for server certificate <{}>", entry.serverCertFingerprint());
130+
return entry.privateKey();
131+
}
132+
LOG.warn("Returning null private key for alias <{}>", alias);
133+
return null;
134+
}
135+
136+
@Override
137+
public String[] getClientAliases(String keyType, Principal[] issuers) {
138+
return null;
139+
}
140+
141+
@Override
142+
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
143+
return null;
144+
}
145+
146+
@Override
147+
public String[] getServerAliases(String keyType, Principal[] issuers) {
148+
return null;
149+
}
150+
}

graylog2-server/src/main/java/org/graylog/collectors/CollectorCaService.java

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,17 @@
1616
*/
1717
package org.graylog.collectors;
1818

19-
import io.netty.handler.ssl.ClientAuth;
20-
import io.netty.handler.ssl.SslContextBuilder;
21-
import io.netty.handler.ssl.SslProvider;
2219
import jakarta.inject.Inject;
2320
import jakarta.inject.Singleton;
2421
import org.bouncycastle.asn1.x509.KeyPurposeId;
2522
import org.bouncycastle.asn1.x509.KeyUsage;
2623
import org.graylog.security.pki.Algorithm;
2724
import org.graylog.security.pki.CertificateEntry;
2825
import org.graylog.security.pki.CertificateService;
29-
import org.graylog.security.pki.PemUtils;
3026
import org.graylog2.plugin.cluster.ClusterIdService;
3127
import org.slf4j.Logger;
3228
import org.slf4j.LoggerFactory;
3329

34-
import java.security.PrivateKey;
35-
import java.security.cert.X509Certificate;
3630
import java.time.Duration;
3731
import java.util.List;
3832

@@ -110,43 +104,6 @@ public CertificateEntry getOtlpServerCert() {
110104
return certificateService.findById(ensureConfig().otlpServerCertId()).orElseThrow(this::caNotInitializedError);
111105
}
112106

113-
/**
114-
* Creates a new {@link SslContextBuilder} configured for the OTLP server endpoint.
115-
* <p>
116-
* The builder is configured with:
117-
* <ul>
118-
* <li>The OTLP server certificate and private key for server identity</li>
119-
* <li>Client authentication required (mTLS)</li>
120-
* <li>The signing cert as the trust anchor for validating client certificates</li>
121-
* </ul>
122-
*
123-
* @return a configured SslContextBuilder ready to be built
124-
*/
125-
public SslContextBuilder newServerSslContextBuilder() {
126-
final var hierarchy = loadHierarchy();
127-
final var otlpServerCert = hierarchy.otlpServerCert();
128-
final var signingCert = hierarchy.signingCert();
129-
130-
try {
131-
final PrivateKey key = PemUtils.parsePrivateKey(certificateService.encryptedValueService().decrypt(otlpServerCert.privateKey()));
132-
133-
final X509Certificate signingCertPem = PemUtils.parseCertificate(signingCert.certificate());
134-
final X509Certificate serverCertPem = PemUtils.parseCertificate(otlpServerCert.certificate());
135-
final X509Certificate trustedCert = PemUtils.parseCertificate(signingCert.certificate());
136-
137-
// The Collector only has access to the CA cert, so we need to have the intermediate signing cert
138-
// in the key cert chain.
139-
return SslContextBuilder.forServer(key, serverCertPem, signingCertPem)
140-
// JDK provider required: BoringSSL (OPENSSL) can load Ed25519 keys but cannot
141-
// complete TLS handshakes — its cipher suite negotiation doesn't recognize Ed25519.
142-
.sslProvider(SslProvider.JDK)
143-
.clientAuth(ClientAuth.REQUIRE)
144-
.trustManager(trustedCert);
145-
} catch (Exception e) {
146-
throw new RuntimeException("Failed to create OTLP server SSL context", e);
147-
}
148-
}
149-
150107
/**
151108
* Loads the existing Collector CA hierarchy.
152109
*
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright (C) 2020 Graylog, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the Server Side Public License, version 1,
6+
* as published by MongoDB, Inc.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* Server Side Public License for more details.
12+
*
13+
* You should have received a copy of the Server Side Public License
14+
* along with this program. If not, see
15+
* <http://www.mongodb.com/licensing/server-side-public-license>.
16+
*/
17+
package org.graylog.collectors;
18+
19+
import org.slf4j.Logger;
20+
import org.slf4j.LoggerFactory;
21+
22+
import javax.net.ssl.X509TrustManager;
23+
import java.security.cert.CertificateException;
24+
import java.security.cert.X509Certificate;
25+
26+
public class CollectorCaTrustManager implements X509TrustManager {
27+
private static final Logger LOG = LoggerFactory.getLogger(CollectorCaTrustManager.class);
28+
29+
@Override
30+
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
31+
LOG.info("CLIENT {} s={}", x509Certificates, s);
32+
}
33+
34+
@Override
35+
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
36+
}
37+
38+
@Override
39+
public X509Certificate[] getAcceptedIssuers() {
40+
return new X509Certificate[0];
41+
}
42+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright (C) 2020 Graylog, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the Server Side Public License, version 1,
6+
* as published by MongoDB, Inc.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* Server Side Public License for more details.
12+
*
13+
* You should have received a copy of the Server Side Public License
14+
* along with this program. If not, see
15+
* <http://www.mongodb.com/licensing/server-side-public-license>.
16+
*/
17+
package org.graylog.collectors;
18+
19+
import io.netty.handler.ssl.ClientAuth;
20+
import io.netty.handler.ssl.SslContextBuilder;
21+
import io.netty.handler.ssl.SslProvider;
22+
import jakarta.inject.Inject;
23+
import org.graylog.security.pki.PemUtils;
24+
25+
import java.security.cert.X509Certificate;
26+
27+
public class CollectorTLSUtils {
28+
private final CollectorCaService caService;
29+
private final CollectorCaKeyManager keyManager;
30+
31+
@Inject
32+
public CollectorTLSUtils(CollectorCaService caService, CollectorCaKeyManager keyManager) {
33+
this.caService = caService;
34+
this.keyManager = keyManager;
35+
}
36+
37+
/**
38+
* Creates a new {@link SslContextBuilder} configured for the OTLP server endpoint.
39+
* <p>
40+
* The builder is configured with:
41+
* <ul>
42+
* <li>The OTLP server certificate and private key for server identity</li>
43+
* <li>Client authentication required (mTLS)</li>
44+
* <li>The signing cert as the trust anchor for validating client certificates</li>
45+
* </ul>
46+
*
47+
* @return a configured SslContextBuilder ready to be built
48+
*/
49+
public SslContextBuilder newServerSslContextBuilder() {
50+
final var signingCert = caService.getSigningCert();
51+
52+
try {
53+
final X509Certificate trustedCert = PemUtils.parseCertificate(signingCert.certificate());
54+
55+
// The Collector only has access to the CA cert, so we need to have the intermediate signing cert
56+
// in the key cert chain.
57+
return SslContextBuilder.forServer(keyManager)
58+
// JDK provider required: BoringSSL (OPENSSL) can load Ed25519 keys but cannot
59+
// complete TLS handshakes — its cipher suite negotiation doesn't recognize Ed25519.
60+
.sslProvider(SslProvider.JDK)
61+
.clientAuth(ClientAuth.REQUIRE)
62+
.trustManager(new CollectorCaTrustManager());
63+
} catch (Exception e) {
64+
throw new RuntimeException("Failed to create OTLP server SSL context", e);
65+
}
66+
}
67+
}

graylog2-server/src/main/java/org/graylog/collectors/CollectorsConfigService.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@
1818

1919
import jakarta.inject.Inject;
2020
import jakarta.inject.Singleton;
21+
import org.graylog.collectors.events.CollectorCaConfigUpdated;
22+
import org.graylog2.events.ClusterEventBus;
2123
import org.graylog2.plugin.cluster.ClusterConfigService;
2224

25+
import java.util.Objects;
2326
import java.util.Optional;
2427

2528
/**
@@ -30,10 +33,12 @@ public class CollectorsConfigService {
3033
private static final CollectorsConfig DEFAULT_CONFIG = CollectorsConfig.createDefault("localhost");
3134

3235
private final ClusterConfigService clusterConfigService;
36+
private final ClusterEventBus clusterEventBus;
3337

3438
@Inject
35-
public CollectorsConfigService(ClusterConfigService clusterConfigService) {
39+
public CollectorsConfigService(ClusterConfigService clusterConfigService, ClusterEventBus clusterEventBus) {
3640
this.clusterConfigService = clusterConfigService;
41+
this.clusterEventBus = clusterEventBus;
3742
}
3843

3944
/**
@@ -70,6 +75,16 @@ public int getOpampMaxRequestBodySizeBytes() {
7075
* @param config the config object
7176
*/
7277
public void save(CollectorsConfig config) {
78+
final var existing = get();
79+
7380
clusterConfigService.write(config);
81+
82+
existing.ifPresent(c -> {
83+
if (!Objects.equals(c.caCertId(), config.caCertId())
84+
|| !Objects.equals(c.signingCertId(), config.signingCertId())
85+
|| !Objects.equals(c.otlpServerCertId(), config.otlpServerCertId())) {
86+
clusterEventBus.post(new CollectorCaConfigUpdated());
87+
}
88+
});
7489
}
7590
}

graylog2-server/src/main/java/org/graylog/collectors/CollectorsModule.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ protected void configure() {
104104

105105
// CA
106106
bind(CollectorCaService.class).in(Scopes.SINGLETON);
107+
bind(CollectorCaKeyManager.class).in(Scopes.SINGLETON);
108+
addInitializer(CollectorCaKeyManager.class);
107109

108110
// Collectors config
109111
bind(CollectorsConfigService.class).asEagerSingleton();

0 commit comments

Comments
 (0)