/*
* Copyright (c) 2015 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.systemservices.impl.security;
import java.net.URI;
import java.security.MessageDigest;
import java.security.cert.Certificate;
import java.security.Key;
import java.security.KeyStore;
import java.util.Map;
import com.emc.storageos.security.ssh.PEMUtil;
import com.emc.storageos.systemservices.impl.upgrade.LocalRepository;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import com.emc.storageos.coordinator.client.model.PropertyInfoExt;
import com.emc.storageos.security.keystore.impl.KeyCertificatePairGenerator;
import com.emc.storageos.security.keystore.impl.KeyStoreUtil;
import com.emc.storageos.security.keystore.impl.KeystoreEngine;
import com.emc.storageos.systemservices.impl.client.SysClientFactory;
import com.emc.storageos.systemservices.impl.property.Notifier;
import com.emc.storageos.systemservices.impl.resource.ConfigService;
import com.emc.storageos.systemservices.impl.util.AbstractManager;
import static com.emc.storageos.coordinator.client.model.Constants.HIDDEN_TEXT_MASK;
/**
* Configure ssh and ssl during syssvc booting up.
* 1. Generate ssh keys and config files and put them in system properties for following boots.
* 2. Dump ssl key store and trust store into cached properties in bootfs
* Internally there is a background thread making sure ssl properties on bootfs consistent with ones in zk.
*/
public class SecretsManager extends AbstractManager {
private static final Logger log = LoggerFactory.getLogger(SecretsManager.class);
private static final String NGINX_PUB_KEY = "nginx_pub_key";
private static final String NGINX_PRIV_KEY = "nginx_priv_key";
private static final String NGINX_KEY_HASH = "nginx_key_hash";
private static final String SSL_PROP_TAG = "ssl";
private String key;
private String cert;
private String localKey;
private String keyHash;
private String localKeyHash;
boolean dhInitDone = false;
@Autowired
private SshConfigurator sshConfig;
@Override
protected URI getWakeUpUrl() {
return SysClientFactory.URI_WAKEUP_SECRETS_MANAGER;
}
@Override
protected void innerRun() {
while (doRun) {
log.debug("Main loop: Start");
// Wait for target info initialized
PropertyInfoExt targetInfo = coordinator.getTargetInfo(PropertyInfoExt.class);
if (targetInfo == null) {
log.info("The target info in ZK has not been initialized yet. Waiting...");
retrySleep();
continue;
}
// Step0: Configure ssh if needed. Probably only run once in first boot.
try {
sshConfig.run();
} catch (Exception e) {
log.info("Error during attempt to configure ssh will be retried: {}", e.getMessage());
retrySleep();
continue;
}
// Step1:
log.info("Step1: Sync SSL key and certificate if needed");
try {
syncSslKeyAndCert();
} catch (Exception e) {
log.info("Step1 failed and will be retried: {}", e.getMessage());
retrySleep();
continue;
}
log.info("Step2: Generate DH Params if not yet");
if (!dhInitDone) {
dhInitDone = genDHParam();
}
// Step3: sleep
log.info("Step3: sleep");
longSleep();
}
}
private boolean genDHParam() {
LocalRepository localRepository = LocalRepository.getInstance();
try {
localRepository.genDHParam();
log.info("Reconfiguring SSL related config files");
localRepository.reconfigProperties(SSL_PROP_TAG);
log.info("Invoking SSL notifier");
Notifier.getInstance(SSL_PROP_TAG).doNotify();
} catch (Exception e) {
log.warn("Failed to generate dhparam.", e);
return false;
}
return true;
}
private void syncSslKeyAndCert() throws Exception {
syncTargetKeyAndCert();
syncLocalKey();
if (escapeNewlines(key).equals(localKey) && keyHash.equals(localKeyHash)) {
log.info("Key in coordinatorsvc and local bootfs are sync. No need to rewrite");
return;
}
updateLocalSslProps();
}
private String generateKeyHash(String key) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(key.getBytes());
byte[] digest = md.digest();
return new String(Base64.encodeBase64(digest));
}
private String getAndEncodePrivateKey(KeyStore keyStore) throws Exception {
Key key = keyStore.getKey(KeystoreEngine.ViPR_KEY_AND_CERTIFICATE_ALIAS, null);
return PEMUtil.encodePrivateKey(key.getEncoded());
}
private String getAndEncodeCert(KeyStore keyStore) throws Exception {
Certificate[] certificates = keyStore.getCertificateChain(
KeystoreEngine.ViPR_KEY_AND_CERTIFICATE_ALIAS);
return KeyCertificatePairGenerator.getCertificateChainAsString(certificates);
}
/**
* The newlines in the external file need to be replaced by "\\n"s
* so that: a) it remains a one-liner in the /etc/systool --getprops output
* b) when _get_prop2 is called in /etc/genconfig, they can be converted back
* to actual newlines.
* The reason we must use "\\n" instead of "\n" here is that _get_props2 references
* ${_GENCONFIG_PROPS} without the double quotes
*/
private static String escapeNewlines(String string) {
return string.replace("\n", "\\\\n");
}
private void updateLocalSslProps() throws Exception {
PropertyInfoExt sslProps = new SslPropertyInfo();
sslProps.addProperty(NGINX_PRIV_KEY, escapeNewlines(key));
sslProps.addProperty(NGINX_PUB_KEY, escapeNewlines(cert));
log.info("updating local ssl properties");
localRepository.setSslPropertyInfo(sslProps);
log.info("Reconfiguring SSL related config files");
localRepository.reconfigProperties(SSL_PROP_TAG);
log.info("Invoking SSL notifier");
Notifier.getInstance(SSL_PROP_TAG).doNotify();
sslProps.addProperty(NGINX_KEY_HASH, keyHash);
log.info("updating local ssl key hash property");
localRepository.setSslPropertyInfo(sslProps);
}
private void syncTargetKeyAndCert() throws Exception {
KeyStore viprKeystore = KeyStoreUtil.getViPRKeystore(coordinator.getCoordinatorClient());
key = getAndEncodePrivateKey(viprKeystore);
cert = getAndEncodeCert(viprKeystore);
keyHash = generateKeyHash(key);
}
private void syncLocalKey() throws Exception {
PropertyInfoExt localSslInfo = localRepository.getSslPropertyInfo();
localKey = localSslInfo.getProperty(NGINX_PRIV_KEY);
localKeyHash = localSslInfo.getProperty(NGINX_KEY_HASH);
}
/**
* All SSL properties should be encrypted but certificate_version.
*/
private static class SslPropertyInfo extends PropertyInfoExt {
@Override
public String toString() {
return toString(true);
}
/**
* Different from PropertyInfoExt, if masking property output only depends on the flag withMask
* and regardless to global metadata.
*
* @param withMask if true, replace all encrypted string with HIDDEN_TEXT_MASK,
* otherwise always print the real content.
* @return
*/
@Override
public String toString(boolean withMask) {
StringBuffer sb = new StringBuffer();
for (Map.Entry<String, String> entry : getProperties().entrySet()) {
sb.append(entry.getKey());
sb.append(ENCODING_EQUAL);
// Hide encrypted string in audit log
if (entry.getKey().equals(ConfigService.CERTIFICATE_VERSION)) {
sb.append(entry.getValue());
} else if (withMask) {
sb.append(HIDDEN_TEXT_MASK);
} else {
sb.append(entry.getValue());
}
sb.append(ENCODING_NEWLINE);
}
return sb.toString();
}
}
}