Skip to content

Commit 78a4679

Browse files
committed
Start Collector key manager
1 parent 7dac847 commit 78a4679

7 files changed

Lines changed: 245 additions & 48 deletions

File tree

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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.eventbus.EventBus;
23+
import com.google.common.eventbus.Subscribe;
24+
import com.google.common.util.concurrent.AbstractIdleService;
25+
import jakarta.inject.Inject;
26+
import jakarta.inject.Singleton;
27+
import org.graylog.collectors.events.CollectorCaConfigUpdated;
28+
import org.graylog.security.pki.PemUtils;
29+
import org.graylog2.security.encryption.EncryptedValueService;
30+
import org.slf4j.Logger;
31+
import org.slf4j.LoggerFactory;
32+
33+
import javax.net.ssl.X509KeyManager;
34+
import java.net.Socket;
35+
import java.security.Principal;
36+
import java.security.PrivateKey;
37+
import java.security.cert.X509Certificate;
38+
import java.time.Duration;
39+
40+
@Singleton
41+
public class CollectorCaKeyManager extends AbstractIdleService implements X509KeyManager {
42+
private static final Logger LOG = LoggerFactory.getLogger(CollectorCaKeyManager.class);
43+
44+
private final CollectorCaService caService;
45+
private final EncryptedValueService encryptedValueService;
46+
private final EventBus eventBus;
47+
private final LoadingCache<Integer, CacheEntry> cache;
48+
49+
private record CacheEntry(PrivateKey privateKey, X509Certificate serverCert, X509Certificate signingCert) {
50+
}
51+
52+
@Inject
53+
public CollectorCaKeyManager(CollectorCaService caService,
54+
EncryptedValueService encryptedValueService,
55+
EventBus eventBus) {
56+
this.caService = caService;
57+
this.encryptedValueService = encryptedValueService;
58+
this.eventBus = eventBus;
59+
this.cache = Caffeine.newBuilder()
60+
.expireAfter(Expiry.<Integer, CacheEntry>creating((key, value) -> Duration.ofSeconds(1)))
61+
.maximumSize(1)
62+
.initialCapacity(1)
63+
.build(this::loadCacheKey);
64+
}
65+
66+
private CacheEntry loadCacheKey(Integer key) {
67+
LOG.info("Loading key {}", key);
68+
try {
69+
final var signingCertEntry = caService.getOtlpServerCert();
70+
final var serverCertEntry = caService.getOtlpServerCert();
71+
final var signingCert = PemUtils.parseCertificate(signingCertEntry.certificate());
72+
final var serverCert = PemUtils.parseCertificate(serverCertEntry.certificate());
73+
final var privateKey = PemUtils.parsePrivateKey(encryptedValueService.decrypt(serverCertEntry.privateKey()));
74+
return new CacheEntry(privateKey, serverCert, signingCert);
75+
} catch (Exception e) {
76+
throw new RuntimeException(e);
77+
}
78+
}
79+
80+
@Override
81+
protected void startUp() throws Exception {
82+
eventBus.register(this);
83+
}
84+
85+
@Override
86+
protected void shutDown() throws Exception {
87+
eventBus.unregister(this);
88+
}
89+
90+
@Subscribe
91+
@SuppressWarnings("unused")
92+
public void handleCollectorsConfigEvent(CollectorCaConfigUpdated event) {
93+
cache.invalidateAll();
94+
}
95+
96+
@Override
97+
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
98+
return keyType;
99+
}
100+
101+
@Override
102+
public X509Certificate[] getCertificateChain(String alias) {
103+
LOG.warn("getCertificateChain alias={}", alias);
104+
if ("EdDSA".equals(alias)) {
105+
final var entry = cache.get(0);
106+
return new X509Certificate[]{entry.serverCert(), entry.signingCert()};
107+
}
108+
return null;
109+
}
110+
111+
@Override
112+
public PrivateKey getPrivateKey(String alias) {
113+
LOG.warn("getPrivateKey alias={}", alias);
114+
if ("EdDSA".equals(alias)) {
115+
return cache.get(0).privateKey();
116+
}
117+
return null;
118+
}
119+
120+
@Override
121+
public String[] getClientAliases(String keyType, Principal[] issuers) {
122+
return null;
123+
}
124+
125+
@Override
126+
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
127+
return null;
128+
}
129+
130+
@Override
131+
public String[] getServerAliases(String keyType, Principal[] issuers) {
132+
return null;
133+
}
134+
}

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: 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(trustedCert);
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: 17 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,17 @@ 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+
83+
existing.ifPresent(c -> {
84+
if (!Objects.equals(c.caCertId(), config.caCertId())
85+
|| !Objects.equals(c.signingCertId(), config.signingCertId())
86+
|| !Objects.equals(c.otlpServerCertId(), config.otlpServerCertId())) {
87+
clusterEventBus.post(new CollectorCaConfigUpdated());
88+
}
89+
});
7490
}
7591
}

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();
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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.events;
18+
19+
public record CollectorCaConfigUpdated() {
20+
}

graylog2-server/src/main/java/org/graylog/collectors/input/transport/CollectorIngestHttpTransport.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import io.netty.handler.ssl.SslContext;
2525
import jakarta.inject.Named;
2626
import org.graylog.collectors.CollectorCaService;
27+
import org.graylog.collectors.CollectorTLSUtils;
2728
import org.graylog.collectors.CollectorsConfig;
2829
import org.graylog.collectors.CollectorsConfigService;
2930
import org.graylog.collectors.IngestEndpointConfig;
@@ -60,7 +61,7 @@ public class CollectorIngestHttpTransport extends AbstractHttpTransport {
6061
public static final String NAME = "CollectorIngestHttpTransport";
6162
static final int DEFAULT_HTTP_PORT = 14401;
6263

63-
private final CollectorCaService collectorCaService;
64+
private final CollectorTLSUtils tlsUtils;
6465

6566
@AssistedInject
6667
public CollectorIngestHttpTransport(@Assisted Configuration configuration,
@@ -71,12 +72,12 @@ public CollectorIngestHttpTransport(@Assisted Configuration configuration,
7172
LocalMetricRegistry localMetricRegistry,
7273
TLSProtocolsConfiguration tlsConfiguration,
7374
@Named("trusted_proxies") Set<IpSubnet> trustedProxies,
74-
CollectorCaService collectorCaService,
75+
CollectorTLSUtils tlsUtils,
7576
CollectorsConfigService collectorsConfigService) {
7677
super(buildTransportConfig(collectorsConfigService), eventLoopGroup, eventLoopGroupFactory,
7778
nettyTransportConfiguration, throughputCounter, localMetricRegistry,
7879
tlsConfiguration, trustedProxies, OtlpHttpUtils.LOGS_PATH);
79-
this.collectorCaService = collectorCaService;
80+
this.tlsUtils = tlsUtils;
8081
}
8182

8283
private static Configuration buildTransportConfig(CollectorsConfigService collectorsConfigService) {
@@ -100,7 +101,7 @@ private static Configuration buildTransportConfig(CollectorsConfigService collec
100101
@Override
101102
protected Callable<? extends ChannelHandler> createSslHandler(MessageInput input) {
102103
return () -> {
103-
final SslContext sslContext = collectorCaService.newServerSslContextBuilder().build();
104+
final SslContext sslContext = tlsUtils.newServerSslContextBuilder().build();
104105
return sslContext.newHandler(PooledByteBufAllocator.DEFAULT);
105106
};
106107
}

0 commit comments

Comments
 (0)