rhtyd commented on a change in pull request #2239: CLOUDSTACK-9993: Securing Agents Communications URL: https://github.com/apache/cloudstack/pull/2239#discussion_r134775294
########## File path: server/src/org/apache/cloudstack/ca/CAManagerImpl.java ########## @@ -0,0 +1,427 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.ca; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; + +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.admin.ca.IssueCertificateCmd; +import org.apache.cloudstack.api.command.admin.ca.ListCAProvidersCmd; +import org.apache.cloudstack.api.command.admin.ca.ListCaCertificateCmd; +import org.apache.cloudstack.api.command.admin.ca.ProvisionCertificateCmd; +import org.apache.cloudstack.api.command.admin.ca.RevokeCertificateCmd; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.ca.CAProvider; +import org.apache.cloudstack.framework.ca.Certificate; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.poll.BackgroundPollManager; +import org.apache.cloudstack.poll.BackgroundPollTask; +import org.apache.cloudstack.utils.identity.ManagementServerNode; +import org.apache.cloudstack.utils.security.CertUtils; +import org.apache.log4j.Logger; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +import com.cloud.agent.AgentManager; +import com.cloud.alert.AlertManager; +import com.cloud.certificate.CrlVO; +import com.cloud.certificate.dao.CrlDao; +import com.cloud.event.ActionEvent; +import com.cloud.event.EventTypes; +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.host.Host; +import com.cloud.host.Status; +import com.cloud.host.dao.HostDao; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.exception.CloudRuntimeException; +import com.google.common.base.Strings; + +public class CAManagerImpl extends ManagerBase implements CAManager { + public static final Logger LOG = Logger.getLogger(CAManagerImpl.class); + + @Inject + private CrlDao crlDao; + @Inject + private HostDao hostDao; + @Inject + private AgentManager agentManager; + @Inject + private BackgroundPollManager backgroundPollManager; + @Inject + private AlertManager alertManager; + + private static CAProvider configuredCaProvider; + private static Map<String, CAProvider> caProviderMap = new HashMap<>(); + private static Map<String, Date> alertMap = new ConcurrentHashMap<>(); + private static Map<String, X509Certificate> activeCertMap = new ConcurrentHashMap<>(); + + private List<CAProvider> caProviders; + + private CAProvider getConfiguredCaProvider() { + if (configuredCaProvider == null && caProviderMap.containsKey(CAProviderPlugin.value())) { + configuredCaProvider = caProviderMap.get(CAProviderPlugin.value()); + } + if (configuredCaProvider == null) { + throw new CloudRuntimeException("Failed to find default configured CA provider plugin"); + } + return configuredCaProvider; + } + + private CAProvider getCAProvider(final String provider) { + if (Strings.isNullOrEmpty(provider)) { + return getConfiguredCaProvider(); + } + final String caProviderName = provider.toLowerCase(); + if (!caProviderMap.containsKey(caProviderName)) { + throw new CloudRuntimeException(String.format("CA provider plugin '%s' not found", caProviderName)); + } + final CAProvider caProvider = caProviderMap.get(caProviderName); + if (caProvider == null) { + throw new CloudRuntimeException(String.format("CA provider plugin '%s' returned is null", caProviderName)); + } + return caProvider; + } + + /////////////////////////////////////////////////////////// + /////////////// CA Manager API Handlers /////////////////// + /////////////////////////////////////////////////////////// + + @Override + public List<CAProvider> getCaProviders() { + return caProviders; + } + + @Override + public Map<String, X509Certificate> getActiveCertificatesMap() { + return activeCertMap; + } + + @Override + public boolean canProvisionCertificates() { + return getConfiguredCaProvider().canProvisionCertificates(); + } + + @Override + public String getCaCertificate(final String caProvider) throws IOException { + final CAProvider provider = getCAProvider(caProvider); + return CertUtils.x509CertificatesToPem(provider.getCaCertificate()); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_CA_CERTIFICATE_ISSUE, eventDescription = "issuing certificate", async = true) + public Certificate issueCertificate(final String csr, final List<String> domainNames, final List<String> ipAddresses, final Integer validityDuration, final String caProvider) { + CallContext.current().setEventDetails("domain(s): " + domainNames + " addresses: " + ipAddresses); + final CAProvider provider = getCAProvider(caProvider); + Integer validity = CAManager.CertValidityPeriod.value(); + if (validityDuration != null) { + validity = validityDuration; + } + if (Strings.isNullOrEmpty(csr)) { + if (domainNames == null || domainNames.isEmpty()) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "No domains or CSR provided"); + } + return provider.issueCertificate(domainNames, ipAddresses, validity); + } + return provider.issueCertificate(csr, domainNames, ipAddresses, validity); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_CA_CERTIFICATE_REVOKE, eventDescription = "revoking certificate", async = true) + public boolean revokeCertificate(final BigInteger certSerial, final String certCn, final String caProvider) { + CallContext.current().setEventDetails("cert serial: " + certSerial); + final CrlVO crl = crlDao.revokeCertificate(certSerial, certCn); + if (crl != null && crl.getCertSerial().equals(certSerial)) { + final CAProvider provider = getCAProvider(caProvider); + return provider.revokeCertificate(certSerial, certCn); + } + return false; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_CA_CERTIFICATE_PROVISION, eventDescription = "provisioning certificate for host", async = true) + public boolean provisionCertificate(final Host host, final Boolean reconnect, final String caProvider) { + if (host == null) { + throw new CloudRuntimeException("Unable to find valid host to renew certificate for"); + } + CallContext.current().setEventDetails("host id: " + host.getId()); + CallContext.current().putContextParameter(Host.class, host.getUuid()); + final String csr; + try { + csr = generateKeyStoreAndCsr(host, null); + if (Strings.isNullOrEmpty(csr)) { + return false; + } + final Certificate certificate = issueCertificate(csr, Collections.singletonList(host.getName()), Arrays.asList(host.getPrivateIpAddress(), host.getPublicIpAddress(), host.getStorageIpAddress()), CAManager.CertValidityPeriod.value(), caProvider); + return deployCertificate(host, certificate, reconnect, null); + } catch (final AgentUnavailableException | OperationTimedoutException e) { + LOG.error("Host/agent is not available or operation timed out, failed to setup keystore and generate CSR for host/agent id=" + host.getId() + ", due to: ", e); + throw new CloudRuntimeException("Failed to generate keystore and get CSR from the host/agent id=" + host.getId()); + } + } + + @Override + public String generateKeyStoreAndCsr(final Host host, final Map<String, String> sshAccessDetails) throws AgentUnavailableException, OperationTimedoutException { + final SetupKeyStoreCommand cmd = new SetupKeyStoreCommand(CertValidityPeriod.value()); + if (sshAccessDetails != null && !sshAccessDetails.isEmpty()) { + cmd.setAccessDetail(sshAccessDetails); + } + CallContext.current().setEventDetails("generating keystore and CSR for host id: " + host.getId()); + final SetupKeystoreAnswer answer = (SetupKeystoreAnswer) agentManager.send(host.getId(), cmd); + return answer.getCsr(); + } + + @Override + public boolean deployCertificate(final Host host, final Certificate certificate, final Boolean reconnect, final Map<String, String> sshAccessDetails) throws AgentUnavailableException, OperationTimedoutException { + final SetupCertificateCommand cmd = new SetupCertificateCommand(certificate); + if (sshAccessDetails != null && !sshAccessDetails.isEmpty()) { + cmd.setAccessDetail(sshAccessDetails); + } + CallContext.current().setEventDetails("deploying certificate for host id: " + host.getId()); + final SetupCertificateAnswer answer = (SetupCertificateAnswer) agentManager.send(host.getId(), cmd); + if (answer.getResult()) { + CallContext.current().setEventDetails("successfully deployed certificate for host id: " + host.getId()); + } else { + CallContext.current().setEventDetails("failed to deploy certificate for host id: " + host.getId()); + } + + if (answer.getResult()) { + getActiveCertificatesMap().put(host.getPrivateIpAddress(), certificate.getClientCertificate()); + if (sshAccessDetails == null && reconnect != null && reconnect) { + LOG.info(String.format("Successfully setup certificate on host, reconnecting with agent with id=%d, name=%s, address=%s", + host.getId(), host.getName(), host.getPublicIpAddress())); + return agentManager.reconnect(host.getId()); + } + return true; + } + return false; + } + + @Override + public void purgeHostCertificate(final Host host) { + if (host == null) { + return; + } + final String privateAddress = host.getPrivateIpAddress(); + final String publicAddress = host.getPublicIpAddress(); + final Map<String, X509Certificate> activeCertsMap = getActiveCertificatesMap(); + if (!Strings.isNullOrEmpty(privateAddress) && activeCertsMap.containsKey(privateAddress)) { + activeCertsMap.remove(privateAddress); + } + if (!Strings.isNullOrEmpty(publicAddress) && activeCertsMap.containsKey(publicAddress)) { + activeCertsMap.remove(publicAddress); + } + } + + @Override + public void sendAlert(final Host host, final String subject, final String message) { + if (host == null) { + return; + } + alertManager.sendAlert(AlertManager.AlertType.ALERT_TYPE_CA_CERT, + host.getDataCenterId(), host.getPodId(), subject, message); + } + + @Override + public SSLEngine createSSLEngine(final SSLContext sslContext, final String remoteAddress) throws GeneralSecurityException, IOException { + if (sslContext == null) { + throw new CloudRuntimeException("SSLContext provided to create SSLEngine is null, aborting"); + } + if (Strings.isNullOrEmpty(remoteAddress)) { + throw new CloudRuntimeException("Remote client address connecting to mgmt server cannot be empty/null"); + } + return getConfiguredCaProvider().createSSLEngine(sslContext, remoteAddress, getActiveCertificatesMap()); + } + + //////////////////////////////////////////////////// + /////////////// CA Manager Setup /////////////////// + //////////////////////////////////////////////////// + + public static final class CABackgroundTask extends ManagedContextRunnable implements BackgroundPollTask { + private CAManager caManager; + private HostDao hostDao; + + public CABackgroundTask(final CAManager caManager, final HostDao hostDao) { + this.caManager = caManager; + this.hostDao = hostDao; + } + + @Override + protected void runInContext() { Review comment: I would avoid, not huge. ---------------------------------------------------------------- This is an automated message from the Apache Git Service. To respond to the message, please log on GitHub and use the URL above to go to the specific comment. For queries about this service, please contact Infrastructure at: [email protected] With regards, Apache Git Services
