aboutsummaryrefslogtreecommitdiffstats
path: root/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/AthenzSslKeyStoreConfigurator.java
blob: 706f797cd2c67924191fcd674c395f89449f44ed (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
121
122
123
124
125
126
127
128
129
130
// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.athenz.instanceproviderservice;

import com.google.inject.Inject;
import com.yahoo.component.AbstractComponent;
import com.yahoo.config.provision.Zone;
import com.yahoo.jdisc.http.ssl.SslKeyStoreConfigurator;
import com.yahoo.jdisc.http.ssl.SslKeyStoreContext;
import com.yahoo.log.LogLevel;
import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig;
import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.AthenzCertificateClient;

import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;

import static com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.Utils.getZoneConfig;

/**
 * @author bjorncs
 */
// TODO Cache certificate on disk
@SuppressWarnings("unused") // Component injected into Jetty connector factory
public class AthenzSslKeyStoreConfigurator extends AbstractComponent implements SslKeyStoreConfigurator {
    private static final Logger log = Logger.getLogger(AthenzSslKeyStoreConfigurator.class.getName());
    // TODO Make expiry and update frequency configurable parameters
    private static final Duration CERTIFICATE_EXPIRY_TIME = Duration.ofDays(30);
    private static final Duration CERTIFICATE_UPDATE_PERIOD = Duration.ofDays(7);
    private static final String DUMMY_PASSWORD = "athenz";

    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    private final AthenzCertificateClient certificateClient;
    private final KeyProvider keyProvider;
    private final AthenzProviderServiceConfig.Zones zoneConfig;
    private final AtomicBoolean alreadyConfigured = new AtomicBoolean();
    private KeyStore initialKeyStore;

    @Inject
    public AthenzSslKeyStoreConfigurator(KeyProvider keyProvider,
                                         AthenzProviderServiceConfig config,
                                         Zone zone) {
        AthenzProviderServiceConfig.Zones zoneConfig = getZoneConfig(config, zone);
        this.certificateClient = new AthenzCertificateClient(config, zoneConfig);
        this.keyProvider = keyProvider;
        this.zoneConfig = zoneConfig;
        this.initialKeyStore = downloadCertificate(keyProvider, certificateClient, zoneConfig);
    }

    @Override
    public void configure(SslKeyStoreContext sslKeyStoreContext) {
        if (alreadyConfigured.getAndSet(true)) { // For debugging purpose of SslKeyStoreConfigurator interface
            throw new IllegalStateException("Already configured. configure() can only be called once.");
        }
        sslKeyStoreContext.updateKeyStore(initialKeyStore, DUMMY_PASSWORD);
        initialKeyStore = null;
        scheduler.scheduleAtFixedRate(new AthenzCertificateUpdater(sslKeyStoreContext),
                                      CERTIFICATE_UPDATE_PERIOD.toMinutes()/*initial delay*/,
                                      CERTIFICATE_UPDATE_PERIOD.toMinutes(),
                                      TimeUnit.MINUTES);
    }

    @Override
    public void deconstruct() {
        try {
            scheduler.shutdownNow();
            scheduler.awaitTermination(30, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            throw new RuntimeException("Failed to shutdown Athenz certificate updater on time", e);
        }
    }

    private static KeyStore downloadCertificate(KeyProvider keyProvider,
                                                AthenzCertificateClient certificateClient,
                                                AthenzProviderServiceConfig.Zones zoneConfig) {
        try {
            PrivateKey privateKey = keyProvider.getPrivateKey(zoneConfig.secretVersion());
            X509Certificate certificate = certificateClient.updateCertificate(privateKey, CERTIFICATE_EXPIRY_TIME);
            verifyActualExpiry(certificate);

            KeyStore keyStore = KeyStore.getInstance("JKS");
            keyStore.load(null);
            keyStore.setKeyEntry("athenz", privateKey, DUMMY_PASSWORD.toCharArray(), new Certificate[]{certificate});
            return keyStore;
        } catch (IOException | NoSuchAlgorithmException | CertificateException | KeyStoreException e) {
            throw new RuntimeException(e);
        }
    }

    private static void verifyActualExpiry(X509Certificate certificate) {
        Duration actualExpiry =
                Duration.between(certificate.getNotBefore().toInstant(),  certificate.getNotAfter().toInstant());
        if (CERTIFICATE_EXPIRY_TIME.compareTo(actualExpiry) > 0) {
            log.log(LogLevel.WARNING,
                    String.format("Expected expiry %s, got %s", CERTIFICATE_EXPIRY_TIME, actualExpiry));
        }
    }

    private class AthenzCertificateUpdater implements Runnable {

        private final SslKeyStoreContext sslKeyStoreContext;

        AthenzCertificateUpdater(SslKeyStoreContext sslKeyStoreContext) {
            this.sslKeyStoreContext = sslKeyStoreContext;
        }

        @Override
        public void run() {
            try {
                log.log(LogLevel.INFO, "Updating Athenz certificate from ZTS");
                KeyStore keyStore = downloadCertificate(keyProvider, certificateClient, zoneConfig);
                sslKeyStoreContext.updateKeyStore(keyStore, DUMMY_PASSWORD);
                log.log(LogLevel.INFO, "Athenz certificate reload successfully completed");
            } catch (Throwable e) {
                log.log(LogLevel.ERROR, "Failed to update certificate from ZTS: " + e.getMessage(), e);
            }
        }

    }
}