summaryrefslogtreecommitdiffstats
path: root/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/SslConfigServerApiImpl.java
blob: 8c2b87f406846b697f3632e154d89faf1b3961b2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.node.admin.configserver;

import com.yahoo.vespa.athenz.tls.AthenzIdentityVerifier;
import com.yahoo.vespa.athenz.tls.AthenzSslContextBuilder;
import com.yahoo.vespa.hosted.node.admin.component.Environment;
import com.yahoo.vespa.hosted.node.admin.configserver.certificate.ConfigServerKeyStoreRefresher;
import com.yahoo.vespa.hosted.node.admin.util.KeyStoreOptions;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import java.security.Security;
import java.util.Collections;
import java.util.Optional;

/**
 * ConfigServerApi with proper keystore, truststore and hostname verifier to communicate with the
 * configserver(s). The keystore is refreshed automatically.
 *
 * @author freva
 */
public class SslConfigServerApiImpl implements ConfigServerApi {

    private final ConfigServerApiImpl configServerApi;
    private final Environment environment;
    private final Optional<ConfigServerKeyStoreRefresher> keyStoreRefresher;

    public SslConfigServerApiImpl(Environment environment) {
        Security.addProvider(new BouncyCastleProvider());

        this.environment = environment;

        // At this point we don't know the state of the keystore, it may not exist at all, or the keystore
        // maybe exists, but the certificate in it is expired. Create the ConfigServerApi without a keystore
        // (but with truststore and hostname verifier).
        this.configServerApi = new ConfigServerApiImpl(
                environment.getConfigServerUris(), makeSslConnectionSocketFactory(Optional.empty()));

        // If we have keystore options, we should make sure we use the keystore with the latest certificate,
        // start the keystore refresher.
        this.keyStoreRefresher = environment.getKeyStoreOptions().map(keyStoreOptions -> {
            // Any callback from KeyStoreRefresher should result in using the latest keystore on disk
            Runnable connectionFactoryRefresher = () -> configServerApi.setSSLConnectionSocketFactory(
                    makeSslConnectionSocketFactory(Optional.of(keyStoreOptions)));

            ConfigServerKeyStoreRefresher keyStoreRefresher = new ConfigServerKeyStoreRefresher(
                    keyStoreOptions, connectionFactoryRefresher, configServerApi);

            // Run the refresh once manually to make sure that we have a valid certificate, otherwise fail.
            try {
                keyStoreRefresher.refreshKeyStoreIfNeeded();
                connectionFactoryRefresher.run(); // Update connectionFactory with the keystore on disk
            } catch (Exception e) {
                throw new RuntimeException("Failed to acquire certificate to config server", e);
            }

            keyStoreRefresher.start();
            return keyStoreRefresher;
        });
    }

    @Override
    public <T> T get(String path, Class<T> wantedReturnType) {
        return configServerApi.get(path, wantedReturnType);
    }

    @Override
    public <T> T post(String path, Object bodyJsonPojo, Class<T> wantedReturnType) {
        return configServerApi.post(path, bodyJsonPojo, wantedReturnType);
    }

    @Override
    public <T> T put(String path, Optional<Object> bodyJsonPojo, Class<T> wantedReturnType) {
        return configServerApi.put(path, bodyJsonPojo, wantedReturnType);
    }

    @Override
    public <T> T patch(String path, Object bodyJsonPojo, Class<T> wantedReturnType) {
        return configServerApi.patch(path, bodyJsonPojo, wantedReturnType);
    }

    @Override
    public <T> T delete(String path, Class<T> wantedReturnType) {
        return configServerApi.delete(path, wantedReturnType);
    }

    @Override
    public void close() {
        keyStoreRefresher.ifPresent(ConfigServerKeyStoreRefresher::stop);
        configServerApi.close();
    }

    private SSLConnectionSocketFactory makeSslConnectionSocketFactory(Optional<KeyStoreOptions> keyStoreOptions) {
        return new SSLConnectionSocketFactory(makeSslContext(keyStoreOptions), makeHostnameVerifier());
    }

    private SSLContext makeSslContext(Optional<KeyStoreOptions> keyStoreOptions) {
        AthenzSslContextBuilder sslContextBuilder = new AthenzSslContextBuilder();
        environment.getTrustStoreOptions().ifPresent(options ->
                sslContextBuilder.withTrustStore(options.path.toFile(), options.type));

        keyStoreOptions.ifPresent(options -> {
            try {
                sslContextBuilder.withKeyStore(options.loadKeyStore(), options.password);
            } catch (Exception e) {
                throw new RuntimeException("Failed to read key store", e);
            }
        });

        return sslContextBuilder.build();
    }

    private HostnameVerifier makeHostnameVerifier() {
        return environment.getAthenzIdentity()
                .map(identity -> (HostnameVerifier) new AthenzIdentityVerifier(Collections.singleton(identity)))
                .orElseGet(SSLConnectionSocketFactory::getDefaultHostnameVerifier);
    }
}