From 179c6a789d2511adf9e2537bd373788a2512ffca Mon Sep 17 00:00:00 2001 From: Sid Kattoju Date: Thu, 22 Apr 2021 15:17:52 -0400 Subject: [PATCH] ikev2 vpn backed by vault pkiengine --- api/pom.xml | 4 + .../exception/RemoteAccessVpnException.java | 28 ++ .../com/cloud/network/RemoteAccessVpn.java | 4 + .../network/vpn/RemoteAccessVpnService.java | 11 +- .../user/vpn/CreateRemoteAccessVpnCmd.java | 5 + .../ListRemoteAccessVpnCaCertificatesCmd.java | 105 ++++++ .../api/response/RemoteAccessVpnResponse.java | 16 + .../org/apache/cloudstack/pki/PkiDetail.java | 74 ++++ .../org/apache/cloudstack/pki/PkiManager.java | 55 +++ .../routing/RemoteAccessVpnCfgCommand.java | 45 ++- .../agent/api/routing/VpnUsersCfgCommand.java | 9 +- .../facade/RemoteAccessVpnConfigItem.java | 17 +- .../facade/VpnUsersConfigItem.java | 2 +- .../virtualnetwork/model/RemoteAccessVpn.java | 45 ++- .../virtualnetwork/model/VpnUserList.java | 12 +- .../VirtualRoutingResourceTest.java | 6 +- .../cloud/network/dao/RemoteAccessVpnVO.java | 24 +- .../RemoteAccessVpnDetailVO.java | 2 +- .../dao/RemoteAccessVpnDetailsDao.java | 4 +- .../dao/RemoteAccessVpnDetailsDaoImpl.java | 27 +- .../META-INF/db/schema-41120to41200.sql.rej | 15 + .../META-INF/db/schema-41510to41600.sql | 8 + pom.xml | 5 + server/pom.xml | 4 + .../java/com/cloud/api/ApiResponseHelper.java | 2 + .../ExternalFirewallDeviceManagerImpl.java | 25 +- .../network/router/CommandSetupHelper.java | 28 +- .../vpn/RemoteAccessVpnManagerImpl.java | 87 ++++- .../org/apache/cloudstack/pki/PkiConfig.java | 120 +++++++ .../org/apache/cloudstack/pki/PkiEngine.java | 52 +++ .../cloudstack/pki/PkiEngineDefault.java | 47 +++ .../apache/cloudstack/pki/PkiEngineVault.java | 329 ++++++++++++++++++ .../apache/cloudstack/pki/PkiManagerImpl.java | 103 ++++++ systemvm/debian/etc/ipsec.d/ikev2.conf | 26 ++ systemvm/debian/opt/cloud/bin/configure.py | 151 +++++++- .../opt/cloud/bin/cs_remoteaccessvpn.py | 6 +- systemvm/debian/opt/cloud/bin/cs_vpnusers.py | 4 + 37 files changed, 1458 insertions(+), 49 deletions(-) create mode 100644 api/src/main/java/com/cloud/exception/RemoteAccessVpnException.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/vpn/ListRemoteAccessVpnCaCertificatesCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/pki/PkiDetail.java create mode 100644 api/src/main/java/org/apache/cloudstack/pki/PkiManager.java create mode 100644 engine/schema/src/main/resources/META-INF/db/schema-41120to41200.sql.rej create mode 100644 server/src/main/java/org/apache/cloudstack/pki/PkiConfig.java create mode 100644 server/src/main/java/org/apache/cloudstack/pki/PkiEngine.java create mode 100644 server/src/main/java/org/apache/cloudstack/pki/PkiEngineDefault.java create mode 100644 server/src/main/java/org/apache/cloudstack/pki/PkiEngineVault.java create mode 100644 server/src/main/java/org/apache/cloudstack/pki/PkiManagerImpl.java create mode 100644 systemvm/debian/etc/ipsec.d/ikev2.conf diff --git a/api/pom.xml b/api/pom.xml index 863236ae3ebc..2894a0ed6fdc 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -66,6 +66,10 @@ cloud-framework-direct-download ${project.version} + + com.bettercloud + vault-java-driver + diff --git a/api/src/main/java/com/cloud/exception/RemoteAccessVpnException.java b/api/src/main/java/com/cloud/exception/RemoteAccessVpnException.java new file mode 100644 index 000000000000..d308a5ef931a --- /dev/null +++ b/api/src/main/java/com/cloud/exception/RemoteAccessVpnException.java @@ -0,0 +1,28 @@ +// 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 com.cloud.exception; + +/** + * @since 4.12.0.0 + */ +public class RemoteAccessVpnException extends ManagementServerException { + private static final long serialVersionUID = -5851224796385227880L; + + public RemoteAccessVpnException(String message) { + super(message); + } +} diff --git a/api/src/main/java/com/cloud/network/RemoteAccessVpn.java b/api/src/main/java/com/cloud/network/RemoteAccessVpn.java index 25b4fbbcdeba..52257e58bbea 100644 --- a/api/src/main/java/com/cloud/network/RemoteAccessVpn.java +++ b/api/src/main/java/com/cloud/network/RemoteAccessVpn.java @@ -32,6 +32,8 @@ enum State { String getIpsecPresharedKey(); + String getCaCertificate(); + String getLocalIp(); Long getNetworkId(); @@ -42,4 +44,6 @@ enum State { @Override boolean isDisplay(); + + String getVpnType(); } diff --git a/api/src/main/java/com/cloud/network/vpn/RemoteAccessVpnService.java b/api/src/main/java/com/cloud/network/vpn/RemoteAccessVpnService.java index 5426d181e70f..f2166196b197 100644 --- a/api/src/main/java/com/cloud/network/vpn/RemoteAccessVpnService.java +++ b/api/src/main/java/com/cloud/network/vpn/RemoteAccessVpnService.java @@ -22,6 +22,7 @@ import org.apache.cloudstack.api.command.user.vpn.ListVpnUsersCmd; import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.RemoteAccessVpnException; import com.cloud.exception.ResourceUnavailableException; import com.cloud.network.RemoteAccessVpn; import com.cloud.network.VpnUser; @@ -29,9 +30,15 @@ import com.cloud.utils.Pair; public interface RemoteAccessVpnService { - static final String RemoteAccessVpnClientIpRangeCK = "remote.access.vpn.client.iprange"; + enum Type { + L2TP, IKEV2 + } - RemoteAccessVpn createRemoteAccessVpn(long vpnServerAddressId, String ipRange, boolean openFirewall, Boolean forDisplay) throws NetworkRuleConflictException; + String RemoteAccessVpnTypeConfigKey = "remote.access.vpn.type"; + String RemoteAccessVpnClientIpRangeCK = "remote.access.vpn.client.iprange"; + + RemoteAccessVpn createRemoteAccessVpn(long vpnServerAddressId, String ipRange, boolean openFirewall, Boolean forDisplay) + throws NetworkRuleConflictException, RemoteAccessVpnException; boolean destroyRemoteAccessVpnForIp(long ipId, Account caller, boolean forceCleanup) throws ResourceUnavailableException; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/CreateRemoteAccessVpnCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/CreateRemoteAccessVpnCmd.java index 9508fa50e355..dc440557571b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/CreateRemoteAccessVpnCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/CreateRemoteAccessVpnCmd.java @@ -33,6 +33,7 @@ import com.cloud.event.EventTypes; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.RemoteAccessVpnException; import com.cloud.exception.ResourceUnavailableException; import com.cloud.network.IpAddress; import com.cloud.network.RemoteAccessVpn; @@ -156,6 +157,10 @@ public void create() { s_logger.info("Network rule conflict: " + e.getMessage()); s_logger.trace("Network Rule Conflict: ", e); throw new ServerApiException(ApiErrorCode.NETWORK_RULE_CONFLICT_ERROR, e.getMessage()); + } catch (RemoteAccessVpnException e) { + s_logger.info("Create vpn internal error: " + e.getMessage()); + s_logger.trace("Create vpn internal error: ", e); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/ListRemoteAccessVpnCaCertificatesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/ListRemoteAccessVpnCaCertificatesCmd.java new file mode 100644 index 000000000000..285a80f654bf --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/ListRemoteAccessVpnCaCertificatesCmd.java @@ -0,0 +1,105 @@ +//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.api.command.user.vpn; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.CertificateResponse; +import org.apache.cloudstack.pki.PkiDetail; +import org.apache.cloudstack.pki.PkiManager; + +import com.cloud.domain.Domain; +import com.cloud.exception.RemoteAccessVpnException; +import com.cloud.user.Account; +import com.cloud.user.DomainService; +import com.cloud.utils.exception.CloudRuntimeException; + +/** +* @author Khosrow Moossavi +* @since 4.12.0.0 +*/ +@APICommand( + name = ListRemoteAccessVpnCaCertificatesCmd.APINAME, + description = "Lists the CA public certificate(s) as support by the configured/provided CA plugin", + responseObject = CertificateResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.12.0.0", + authorized = { + RoleType.Admin, + RoleType.ResourceAdmin, + RoleType.DomainAdmin, + RoleType.User + } +) +public class ListRemoteAccessVpnCaCertificatesCmd extends BaseCmd { + public static final String APINAME = "listVpnCaCertificate"; + + @Inject private DomainService domainService; + @Inject private PkiManager pkiManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.DOMAIN, type = CommandType.STRING, description = "Name of the CA service provider, otherwise the default configured provider plugin will be used") + private String domain; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getDomain() { + return domain; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + final PkiDetail certificate; + + try { + Domain domain = domainService.getDomain(getDomain()); + certificate = pkiManager.getCertificate(domain); + } catch (final RemoteAccessVpnException e) { + throw new CloudRuntimeException("Failed to get CA certificates for given domain"); + } + + final CertificateResponse certificateResponse = new CertificateResponse("cacertificates"); + certificateResponse.setCertificate(certificate.getIssuingCa()); + certificateResponse.setResponseName(getCommandName()); + setResponseObject(certificateResponse); + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_TYPE_NORMAL; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/RemoteAccessVpnResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/RemoteAccessVpnResponse.java index 0e078bea5bd7..baf2e7623f36 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/RemoteAccessVpnResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/RemoteAccessVpnResponse.java @@ -77,6 +77,14 @@ public class RemoteAccessVpnResponse extends BaseResponse implements ControlledE @Param(description = "is vpn for display to the regular user", since = "4.4", authorized = {RoleType.Admin}) private Boolean forDisplay; + @SerializedName(ApiConstants.TYPE) + @Param(description = "the type of remote access vpn implementation") + private String type; + + @SerializedName(ApiConstants.CERTIFICATE) + @Param(description = "the client certificate") + private String certificate; + public void setPublicIp(String publicIp) { this.publicIp = publicIp; } @@ -129,4 +137,12 @@ public void setId(String id) { public void setForDisplay(Boolean forDisplay) { this.forDisplay = forDisplay; } + + public void setType(String type) { + this.type = type; + } + + public void setCertificate(String certificate) { + this.certificate = certificate; + } } diff --git a/api/src/main/java/org/apache/cloudstack/pki/PkiDetail.java b/api/src/main/java/org/apache/cloudstack/pki/PkiDetail.java new file mode 100644 index 000000000000..aa8d5d3991de --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/pki/PkiDetail.java @@ -0,0 +1,74 @@ +// 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.pki; + +/** + * @author Khosrow Moossavi + * @since 4.12.0.0 + */ +public class PkiDetail { + private String certificate; + private String issuingCa; + private String privateKey; + private String privateKeyType; + private String serialNumber; + + public PkiDetail certificate(final String certificate) { + this.certificate = certificate; + return this; + } + + public PkiDetail issuingCa(final String issuingCa) { + this.issuingCa = issuingCa; + return this; + } + + public PkiDetail privateKey(final String privateKey) { + this.privateKey = privateKey; + return this; + } + + public PkiDetail privateKeyType(final String privateKeyType) { + this.privateKeyType = privateKeyType; + return this; + } + + public PkiDetail serialNumber(final String serialNumber) { + this.serialNumber = serialNumber; + return this; + } + + public String getCertificate() { + return certificate; + } + + public String getIssuingCa() { + return issuingCa; + } + + public String getPrivateKey() { + return privateKey; + } + + public String getPrivateKeyType() { + return privateKeyType; + } + + public String getSerialNumber() { + return serialNumber; + } +} \ No newline at end of file diff --git a/api/src/main/java/org/apache/cloudstack/pki/PkiManager.java b/api/src/main/java/org/apache/cloudstack/pki/PkiManager.java new file mode 100644 index 000000000000..cf19e3c54808 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/pki/PkiManager.java @@ -0,0 +1,55 @@ +// 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.pki; + +import com.cloud.domain.Domain; +import com.cloud.exception.RemoteAccessVpnException; +import com.cloud.utils.net.Ip; + +/** + * @author Khosrow Moossavi + * @since 4.12.0.0 + */ +public interface PkiManager { + String CREDENTIAL_ISSUING_CA = "credential.issuing.ca"; + String CREDENTIAL_SERIAL_NUMBER = "credential.serial.number"; + String CREDENTIAL_CERTIFICATE = "credential.certificate"; + String CREDENTIAL_PRIVATE_KEY = "credential.private.key"; + + /** + * Issue a Certificate for specific IP and specific Domain act as the CA + * + * @param domain object to extract name and id to be used to issuing CA + * @param publicIp to be included in the certificate + * + * @return detail about just signed PKI, including issuing CA, certificate, private key and serial number + * + * @throws RemoteAccessVpnException + */ + PkiDetail issueCertificate(Domain domain, Ip publicIp) throws RemoteAccessVpnException; + + /** + * Get a Certificate for specific Domain act as the CA + * + * @param domain object to extract its id to be find the issuing CA + * + * @return details about signed PKI, including issuing CA, certificate and serial number + * + * @throws RemoteAccessVpnException + */ + PkiDetail getCertificate(Domain domain) throws RemoteAccessVpnException; +} diff --git a/core/src/main/java/com/cloud/agent/api/routing/RemoteAccessVpnCfgCommand.java b/core/src/main/java/com/cloud/agent/api/routing/RemoteAccessVpnCfgCommand.java index c7dabe5b14d8..b55f5e1f282d 100644 --- a/core/src/main/java/com/cloud/agent/api/routing/RemoteAccessVpnCfgCommand.java +++ b/core/src/main/java/com/cloud/agent/api/routing/RemoteAccessVpnCfgCommand.java @@ -30,6 +30,12 @@ public class RemoteAccessVpnCfgCommand extends NetworkElementCommand { private String localCidr; private String publicInterface; + // items related to VPN IKEv2 implementation + private String vpnType; + private String caCert; + private String serverCert; + private String serverKey; + protected RemoteAccessVpnCfgCommand() { this.create = false; } @@ -43,7 +49,8 @@ public boolean executeInSequence() { return true; } - public RemoteAccessVpnCfgCommand(boolean create, String vpnServerAddress, String localIp, String ipRange, String ipsecPresharedKey, boolean vpcEnabled) { + public RemoteAccessVpnCfgCommand(boolean create, String vpnServerAddress, String localIp, String ipRange, String ipsecPresharedKey, boolean vpcEnabled, String vpnType, + String caCert, String serverCert, String serverKey) { this.vpnServerIp = vpnServerAddress; this.ipRange = ipRange; this.presharedKey = ipsecPresharedKey; @@ -55,6 +62,10 @@ public RemoteAccessVpnCfgCommand(boolean create, String vpnServerAddress, String } else { this.setPublicInterface("eth2"); } + this.vpnType = vpnType; + this.caCert = caCert; + this.serverCert = serverCert; + this.serverKey = serverKey; } public String getVpnServerIp() { @@ -109,4 +120,36 @@ public void setPublicInterface(String publicInterface) { this.publicInterface = publicInterface; } + public String getVpnType() { + return vpnType; + } + + public void setVpnType(String vpnType) { + this.vpnType = vpnType; + } + + public String getCaCert() { + return caCert; + } + + public void setCaCert(String caCert) { + this.caCert = caCert; + } + + public String getServerCert() { + return serverCert; + } + + public void setServerCert(String serverCert) { + this.serverCert = serverCert; + } + + public String getServerKey() { + return serverKey; + } + + public void setServerKey(String serverKey) { + this.serverKey = serverKey; + } + } diff --git a/core/src/main/java/com/cloud/agent/api/routing/VpnUsersCfgCommand.java b/core/src/main/java/com/cloud/agent/api/routing/VpnUsersCfgCommand.java index 3510d14fad52..b4c8d6107786 100644 --- a/core/src/main/java/com/cloud/agent/api/routing/VpnUsersCfgCommand.java +++ b/core/src/main/java/com/cloud/agent/api/routing/VpnUsersCfgCommand.java @@ -79,12 +79,13 @@ public String getUsernamePassword() { } UsernamePassword[] userpwds; + private String vpnType; protected VpnUsersCfgCommand() { } - public VpnUsersCfgCommand(List addUsers, List removeUsers) { + public VpnUsersCfgCommand(List addUsers, List removeUsers, String vpnType) { userpwds = new UsernamePassword[addUsers.size() + removeUsers.size()]; int i = 0; for (VpnUser vpnUser : removeUsers) { @@ -93,6 +94,8 @@ public VpnUsersCfgCommand(List addUsers, List removeUsers) { for (VpnUser vpnUser : addUsers) { userpwds[i++] = new UsernamePassword(vpnUser.getUsername(), vpnUser.getPassword(), true); } + + this.vpnType = vpnType; } @Override @@ -104,4 +107,8 @@ public UsernamePassword[] getUserpwds() { return userpwds; } + public String getVpnType() { + return vpnType; + } + } diff --git a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/RemoteAccessVpnConfigItem.java b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/RemoteAccessVpnConfigItem.java index be51c30745b0..3586eecdbb95 100644 --- a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/RemoteAccessVpnConfigItem.java +++ b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/RemoteAccessVpnConfigItem.java @@ -32,10 +32,21 @@ public class RemoteAccessVpnConfigItem extends AbstractConfigItemFacade { @Override public List generateConfig(final NetworkElementCommand cmd) { - final RemoteAccessVpnCfgCommand command = (RemoteAccessVpnCfgCommand) cmd; + final RemoteAccessVpnCfgCommand command = (RemoteAccessVpnCfgCommand)cmd; + + final RemoteAccessVpn remoteAccessVpn = new RemoteAccessVpn( + command.isCreate(), + command.getIpRange(), + command.getPresharedKey(), + command.getVpnServerIp(), + command.getLocalIp(), + command.getLocalCidr(), + command.getPublicInterface(), + command.getVpnType(), + command.getCaCert(), + command.getServerCert(), + command.getServerKey()); - final RemoteAccessVpn remoteAccessVpn = new RemoteAccessVpn(command.isCreate(), command.getIpRange(), command.getPresharedKey(), command.getVpnServerIp(), command.getLocalIp(), command.getLocalCidr(), - command.getPublicInterface()); return generateConfigItems(remoteAccessVpn); } diff --git a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/VpnUsersConfigItem.java b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/VpnUsersConfigItem.java index c98a93e2d3d0..2dd87c6c1810 100644 --- a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/VpnUsersConfigItem.java +++ b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/VpnUsersConfigItem.java @@ -41,7 +41,7 @@ public List generateConfig(final NetworkElementCommand cmd) { vpnUsers.add(new VpnUser(userpwd.getUsername(), userpwd.getPassword(), userpwd.isAdd())); } - final VpnUserList vpnUserList = new VpnUserList(vpnUsers); + final VpnUserList vpnUserList = new VpnUserList(vpnUsers, command.getVpnType()); return generateConfigItems(vpnUserList); } diff --git a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/RemoteAccessVpn.java b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/RemoteAccessVpn.java index 5b5c05bf7fd7..764136a04fb8 100644 --- a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/RemoteAccessVpn.java +++ b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/RemoteAccessVpn.java @@ -24,11 +24,18 @@ public class RemoteAccessVpn extends ConfigBase { public boolean create; public String ipRange, presharedKey, vpnServerIp, localIp, localCidr, publicInterface; + // items related to VPN IKEv2 implementation + private String vpnType; + private String caCert; + private String serverCert; + private String serverKey; + public RemoteAccessVpn() { super(ConfigBase.REMOTEACCESSVPN); } - public RemoteAccessVpn(boolean create, String ipRange, String presharedKey, String vpnServerIp, String localIp, String localCidr, String publicInterface) { + public RemoteAccessVpn(boolean create, String ipRange, String presharedKey, String vpnServerIp, String localIp, String localCidr, String publicInterface, String vpnType, + String caCert, String serverCert, String serverKey) { super(ConfigBase.REMOTEACCESSVPN); this.create = create; this.ipRange = ipRange; @@ -37,6 +44,10 @@ public RemoteAccessVpn(boolean create, String ipRange, String presharedKey, Stri this.localIp = localIp; this.localCidr = localCidr; this.publicInterface = publicInterface; + this.vpnType = vpnType; + this.caCert = caCert; + this.serverCert = serverCert; + this.serverKey = serverKey; } public boolean isCreate() { @@ -95,4 +106,36 @@ public void setPublicInterface(String publicInterface) { this.publicInterface = publicInterface; } + public String getVpnType() { + return vpnType; + } + + public void setVpnType(String vpnType) { + this.vpnType = vpnType; + } + + public String getCaCert() { + return caCert; + } + + public void setCaCert(String caCert) { + this.caCert = caCert; + } + + public String getServerCert() { + return serverCert; + } + + public void setServerCert(String serverCert) { + this.serverCert = serverCert; + } + + public String getServerKey() { + return serverKey; + } + + public void setServerKey(String serverKey) { + this.serverKey = serverKey; + } + } diff --git a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/VpnUserList.java b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/VpnUserList.java index 115fcc9bd1ef..e122b6336f29 100644 --- a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/VpnUserList.java +++ b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/VpnUserList.java @@ -23,14 +23,16 @@ public class VpnUserList extends ConfigBase { private List vpnUsers; + private String vpnType; public VpnUserList() { super(ConfigBase.VPN_USER_LIST); } - public VpnUserList(List vpnUsers) { + public VpnUserList(List vpnUsers, String vpnType) { super(ConfigBase.VPN_USER_LIST); this.vpnUsers = vpnUsers; + this.setVpnType(vpnType); } public List getVpnUsers() { @@ -41,4 +43,12 @@ public void setVpnUsers(List vpnUsers) { this.vpnUsers = vpnUsers; } + public String getVpnType() { + return vpnType; + } + + public void setVpnType(String vpnType) { + this.vpnType = vpnType; + } + } diff --git a/core/src/test/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResourceTest.java b/core/src/test/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResourceTest.java index 200f266b9251..7a8465e3efc0 100644 --- a/core/src/test/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResourceTest.java +++ b/core/src/test/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResourceTest.java @@ -545,21 +545,21 @@ public void testRemoteAccessVpnCfgCommand() { } protected RemoteAccessVpnCfgCommand generateRemoteAccessVpnCfgCommand1() { - final RemoteAccessVpnCfgCommand cmd = new RemoteAccessVpnCfgCommand(true, "124.10.10.10", "10.10.1.1", "10.10.1.10-10.10.1.20", "sharedkey", false); + final RemoteAccessVpnCfgCommand cmd = new RemoteAccessVpnCfgCommand(true, "124.10.10.10", "10.10.1.1", "10.10.1.10-10.10.1.20", "sharedkey", false, null, null, null, null); cmd.setAccessDetail(NetworkElementCommand.ROUTER_NAME, ROUTERNAME); cmd.setLocalCidr("10.1.1.1/24"); return cmd; } protected RemoteAccessVpnCfgCommand generateRemoteAccessVpnCfgCommand2() { - final RemoteAccessVpnCfgCommand cmd = new RemoteAccessVpnCfgCommand(false, "124.10.10.10", "10.10.1.1", "10.10.1.10-10.10.1.20", "sharedkey", false); + final RemoteAccessVpnCfgCommand cmd = new RemoteAccessVpnCfgCommand(false, "124.10.10.10", "10.10.1.1", "10.10.1.10-10.10.1.20", "sharedkey", false, null, null, null, null); cmd.setAccessDetail(NetworkElementCommand.ROUTER_NAME, ROUTERNAME); cmd.setLocalCidr("10.1.1.1/24"); return cmd; } protected RemoteAccessVpnCfgCommand generateRemoteAccessVpnCfgCommand3() { - final RemoteAccessVpnCfgCommand cmd = new RemoteAccessVpnCfgCommand(true, "124.10.10.10", "10.10.1.1", "10.10.1.10-10.10.1.20", "sharedkey", true); + final RemoteAccessVpnCfgCommand cmd = new RemoteAccessVpnCfgCommand(true, "124.10.10.10", "10.10.1.1", "10.10.1.10-10.10.1.20", "sharedkey", true, null, null, null, null); cmd.setAccessDetail(NetworkElementCommand.ROUTER_NAME, ROUTERNAME); cmd.setLocalCidr("10.1.1.1/24"); return cmd; diff --git a/engine/schema/src/main/java/com/cloud/network/dao/RemoteAccessVpnVO.java b/engine/schema/src/main/java/com/cloud/network/dao/RemoteAccessVpnVO.java index fdb98b9dd9d8..b7584fbec1f1 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/RemoteAccessVpnVO.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/RemoteAccessVpnVO.java @@ -69,11 +69,18 @@ public class RemoteAccessVpnVO implements RemoteAccessVpn { @Column(name = "display", updatable = true, nullable = false) protected boolean display = true; + @Column(name = "vpn_type") + private String vpnType; + + @Encrypt + @Column(name = "ca_certificate", length = 8191) + private String caCertificate; + public RemoteAccessVpnVO() { uuid = UUID.randomUUID().toString(); } - public RemoteAccessVpnVO(long accountId, long domainId, Long networkId, long publicIpId, Long vpcId, String localIp, String ipRange, String presharedKey) { + public RemoteAccessVpnVO(long accountId, long domainId, Long networkId, long publicIpId, Long vpcId, String localIp, String ipRange, String presharedKey, String vpnType) { this.accountId = accountId; serverAddressId = publicIpId; this.ipRange = ipRange; @@ -84,6 +91,7 @@ public RemoteAccessVpnVO(long accountId, long domainId, Long networkId, long pub state = State.Added; uuid = UUID.randomUUID().toString(); this.vpcId = vpcId; + this.vpnType = vpnType; } @Override @@ -166,6 +174,20 @@ public boolean isDisplay() { return display; } + @Override + public String getCaCertificate() { + return caCertificate; + } + + public void setCaCertificate(String caCertificate) { + this.caCertificate = caCertificate; + } + + @Override + public String getVpnType() { + return vpnType; + } + @Override public Class getEntityType() { return RemoteAccessVpn.class; diff --git a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/RemoteAccessVpnDetailVO.java b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/RemoteAccessVpnDetailVO.java index 5fb01a25c2a9..86e50a38787e 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/RemoteAccessVpnDetailVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/RemoteAccessVpnDetailVO.java @@ -39,7 +39,7 @@ public class RemoteAccessVpnDetailVO implements ResourceDetail { @Column(name = "name") private String name; - @Column(name = "value", length = 1024) + @Column(name = "value", length = 8191) private String value; @Column(name = "display") diff --git a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/dao/RemoteAccessVpnDetailsDao.java b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/dao/RemoteAccessVpnDetailsDao.java index 297b7f614c12..d0fdbef84171 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/dao/RemoteAccessVpnDetailsDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/dao/RemoteAccessVpnDetailsDao.java @@ -16,11 +16,13 @@ // under the License. package org.apache.cloudstack.resourcedetail.dao; +import java.util.Map; + import org.apache.cloudstack.resourcedetail.RemoteAccessVpnDetailVO; import org.apache.cloudstack.resourcedetail.ResourceDetailsDao; import com.cloud.utils.db.GenericDao; public interface RemoteAccessVpnDetailsDao extends GenericDao, ResourceDetailsDao { - + Map getDetails(long vpnId); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/dao/RemoteAccessVpnDetailsDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/dao/RemoteAccessVpnDetailsDaoImpl.java index a71b006254e5..71a9aeaf5e71 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/dao/RemoteAccessVpnDetailsDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/dao/RemoteAccessVpnDetailsDaoImpl.java @@ -16,17 +16,42 @@ // under the License. package org.apache.cloudstack.resourcedetail.dao; +import java.util.Map; +import java.util.stream.Collectors; import org.springframework.stereotype.Component; import org.apache.cloudstack.resourcedetail.RemoteAccessVpnDetailVO; import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase; +import com.cloud.utils.crypt.DBEncryptionUtil; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + @Component public class RemoteAccessVpnDetailsDaoImpl extends ResourceDetailsDaoBase implements RemoteAccessVpnDetailsDao { + protected final SearchBuilder vpnSearch; + + public RemoteAccessVpnDetailsDaoImpl() { + super(); + + vpnSearch = createSearchBuilder(); + vpnSearch.and("remote_access_vpn", vpnSearch.entity().getResourceId(), SearchCriteria.Op.EQ); + vpnSearch.done(); + } @Override public void addDetail(long resourceId, String key, String value, boolean display) { - super.addDetail(new RemoteAccessVpnDetailVO(resourceId, key, value, display)); + super.addDetail(new RemoteAccessVpnDetailVO(resourceId, key, DBEncryptionUtil.encrypt(value), display)); + } + + @Override + public Map getDetails(long vpnId) { + SearchCriteria sc = vpnSearch.create(); + sc.setParameters("remote_access_vpn", vpnId); + + return listBy(sc).stream().collect(Collectors.toMap(RemoteAccessVpnDetailVO::getName, detail -> { + return DBEncryptionUtil.decrypt(detail.getValue()); + })); } } diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41120to41200.sql.rej b/engine/schema/src/main/resources/META-INF/db/schema-41120to41200.sql.rej new file mode 100644 index 000000000000..fd4db6a84ccb --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/schema-41120to41200.sql.rej @@ -0,0 +1,15 @@ +diff a/engine/schema/src/main/resources/META-INF/db/schema-41120to41200.sql b/engine/schema/src/main/resources/META-INF/db/schema-41120to41200.sql (rejected hunks) +@@ -34,4 +34,11 @@ INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`, + INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`, `sort_order`) values (UUID(), 3, 'moveNetworkAclItem', 'ALLOW', 302) ON DUPLICATE KEY UPDATE rule=rule; + INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`, `sort_order`) values (UUID(), 4, 'moveNetworkAclItem', 'ALLOW', 260) ON DUPLICATE KEY UPDATE rule=rule; + +-UPDATE `cloud`.`async_job` SET `removed` = now() WHERE `removed` IS NULL; +\ No newline at end of file ++UPDATE `cloud`.`async_job` SET `removed` = now() WHERE `removed` IS NULL; ++ ++-- VPN IKEv2 implementation ++ALTER TABLE `cloud`.`remote_access_vpn` CHANGE COLUMN `ipsec_psk` `ipsec_psk` VARCHAR(256) NULL ; ++ALTER TABLE `cloud`.`remote_access_vpn` ++ ADD COLUMN `vpn_type` VARCHAR(8) NOT NULL AFTER `display`, ++ ADD COLUMN `ca_certificate` VARCHAR(8191) NULL AFTER `vpn_type`; ++ALTER TABLE `cloud`.`remote_access_vpn_details` CHANGE COLUMN `value` `value` VARCHAR(8191) NOT NULL ; diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41510to41600.sql b/engine/schema/src/main/resources/META-INF/db/schema-41510to41600.sql index eec9bcd671b3..cd187222c841 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-41510to41600.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-41510to41600.sql @@ -303,3 +303,11 @@ from -- Update name for global configuration user.vm.readonly.ui.details Update configuration set name='user.vm.readonly.details' where name='user.vm.readonly.ui.details'; + +-- VPN IKEv2 implementation +ALTER TABLE `cloud`.`remote_access_vpn` CHANGE COLUMN `ipsec_psk` `ipsec_psk` VARCHAR(256) NULL ; +ALTER TABLE `cloud`.`remote_access_vpn` + ADD COLUMN `vpn_type` VARCHAR(8) NOT NULL AFTER `display`, + ADD COLUMN `ca_certificate` VARCHAR(8191) NULL AFTER `vpn_type`; +ALTER TABLE `cloud`.`remote_access_vpn_details` CHANGE COLUMN `value` `value` VARCHAR(8191) NOT NULL ; + diff --git a/pom.xml b/pom.xml index c945891719ab..11caf3518dff 100644 --- a/pom.xml +++ b/pom.xml @@ -546,6 +546,11 @@ aspectjweaver 1.8.13 + + com.bettercloud + vault-java-driver + 3.1.0 + org.bouncycastle bcpkix-jdk15on diff --git a/server/pom.xml b/server/pom.xml index 13e7ac79897c..bdd4d77ce1f8 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -170,6 +170,10 @@ org.influxdb influxdb-java + + com.bettercloud + vault-java-driver + diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index ad8672bbd089..6b414a3ece3c 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -1593,10 +1593,12 @@ public RemoteAccessVpnResponse createRemoteAccessVpnResponse(RemoteAccessVpn vpn } vpnResponse.setIpRange(vpn.getIpRange()); vpnResponse.setPresharedKey(vpn.getIpsecPresharedKey()); + vpnResponse.setCertificate(vpn.getCaCertificate()); populateOwner(vpnResponse, vpn); vpnResponse.setState(vpn.getState().toString()); vpnResponse.setId(vpn.getUuid()); vpnResponse.setForDisplay(vpn.isDisplay()); + vpnResponse.setType(vpn.getVpnType()); vpnResponse.setObjectName("remoteaccessvpn"); return vpnResponse; diff --git a/server/src/main/java/com/cloud/network/ExternalFirewallDeviceManagerImpl.java b/server/src/main/java/com/cloud/network/ExternalFirewallDeviceManagerImpl.java index 496359d1d07f..5c160cb6aa3b 100644 --- a/server/src/main/java/com/cloud/network/ExternalFirewallDeviceManagerImpl.java +++ b/server/src/main/java/com/cloud/network/ExternalFirewallDeviceManagerImpl.java @@ -89,6 +89,7 @@ import com.cloud.network.rules.PortForwardingRule; import com.cloud.network.rules.StaticNat; import com.cloud.network.rules.dao.PortForwardingRulesDao; +import com.cloud.network.vpn.RemoteAccessVpnService; import com.cloud.offering.NetworkOffering; import com.cloud.offerings.NetworkOfferingVO; import com.cloud.offerings.dao.NetworkOfferingDao; @@ -705,8 +706,24 @@ public boolean manageRemoteAccessVpn(boolean create, Network network, RemoteAcce String maskedIpRange = ipRange[0] + "-" + ipRange[1]; - RemoteAccessVpnCfgCommand createVpnCmd = - new RemoteAccessVpnCfgCommand(create, ip.getAddress().addr(), vpn.getLocalIp(), maskedIpRange, vpn.getIpsecPresharedKey(), false); + //TODO + final String vpnType = null; + final String caCert = null; + final String serverCert = null; + final String serverKey = null; + + RemoteAccessVpnCfgCommand createVpnCmd = new RemoteAccessVpnCfgCommand( + create, + ip.getAddress().addr(), + vpn.getLocalIp(), + maskedIpRange, + vpn.getIpsecPresharedKey(), + false, + vpnType, + caCert, + serverCert, + serverKey); + createVpnCmd.setAccessDetail(NetworkElementCommand.ACCOUNT_ID, String.valueOf(network.getAccountId())); createVpnCmd.setAccessDetail(NetworkElementCommand.GUEST_NETWORK_CIDR, network.getCidr()); Answer answer = _agentMgr.easySend(externalFirewall.getId(), createVpnCmd); @@ -740,7 +757,9 @@ public boolean manageRemoteAccessVpnUsers(Network network, RemoteAccessVpn vpn, } } - VpnUsersCfgCommand addUsersCmd = new VpnUsersCfgCommand(addUsers, removeUsers); + String vpnType = _configDao.getValue(RemoteAccessVpnService.RemoteAccessVpnTypeConfigKey); + + VpnUsersCfgCommand addUsersCmd = new VpnUsersCfgCommand(addUsers, removeUsers, vpnType); addUsersCmd.setAccessDetail(NetworkElementCommand.ACCOUNT_ID, String.valueOf(network.getAccountId())); addUsersCmd.setAccessDetail(NetworkElementCommand.GUEST_NETWORK_CIDR, network.getCidr()); diff --git a/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java b/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java index e344b462b50a..dab46106e9bb 100644 --- a/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java +++ b/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java @@ -27,6 +27,9 @@ import javax.inject.Inject; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.pki.PkiManager; +import org.apache.cloudstack.resourcedetail.dao.RemoteAccessVpnDetailsDao; + import org.apache.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -107,6 +110,7 @@ import com.cloud.network.vpc.Vpc; import com.cloud.network.vpc.VpcGateway; import com.cloud.network.vpc.dao.VpcDao; +import com.cloud.network.vpn.RemoteAccessVpnService; import com.cloud.offering.NetworkOffering; import com.cloud.offerings.NetworkOfferingVO; import com.cloud.offerings.dao.NetworkOfferingDao; @@ -179,6 +183,8 @@ public class CommandSetupHelper { private RouterControlHelper _routerControlHelper; @Inject private HostDao _hostDao; + @Inject + private RemoteAccessVpnDetailsDao remoteAccessVpnDetailsDao; @Autowired @Qualifier("networkHelper") @@ -211,7 +217,9 @@ public void createApplyVpnUsersCommand(final List users, fina } } - final VpnUsersCfgCommand cmd = new VpnUsersCfgCommand(addUsers, removeUsers); + String vpnType = _configDao.getValue(RemoteAccessVpnService.RemoteAccessVpnTypeConfigKey); + + final VpnUsersCfgCommand cmd = new VpnUsersCfgCommand(addUsers, removeUsers, vpnType); cmd.setAccessDetail(NetworkElementCommand.ACCOUNT_ID, String.valueOf(router.getAccountId())); cmd.setAccessDetail(NetworkElementCommand.ROUTER_IP, _routerControlHelper.getRouterControlIp(router.getId())); cmd.setAccessDetail(NetworkElementCommand.ROUTER_NAME, router.getInstanceName()); @@ -588,8 +596,22 @@ public void createApplyVpnCommands(final boolean isCreate, final RemoteAccessVpn cidr = network.getCidr(); } - final RemoteAccessVpnCfgCommand startVpnCmd = new RemoteAccessVpnCfgCommand(isCreate, ip.getAddress().addr(), vpn.getLocalIp(), vpn.getIpRange(), - vpn.getIpsecPresharedKey(), vpn.getVpcId() != null); + // read additional details from DB and fill them up in RemoteAccessVpnVO + final Map vpnDetials = remoteAccessVpnDetailsDao.getDetails(vpn.getId()); + final String vpnType = _configDao.getValue(RemoteAccessVpnService.RemoteAccessVpnTypeConfigKey); + + final RemoteAccessVpnCfgCommand startVpnCmd = new RemoteAccessVpnCfgCommand( + isCreate, + ip.getAddress().addr(), + vpn.getLocalIp(), + vpn.getIpRange(), + vpn.getIpsecPresharedKey(), + vpn.getVpcId() != null, + vpnType, + vpnDetials.get(PkiManager.CREDENTIAL_ISSUING_CA), + vpnDetials.get(PkiManager.CREDENTIAL_CERTIFICATE), + vpnDetials.get(PkiManager.CREDENTIAL_PRIVATE_KEY)); + startVpnCmd.setLocalCidr(cidr); startVpnCmd.setAccessDetail(NetworkElementCommand.ROUTER_IP, _routerControlHelper.getRouterControlIp(router.getId())); startVpnCmd.setAccessDetail(NetworkElementCommand.ROUTER_NAME, router.getInstanceName()); diff --git a/server/src/main/java/com/cloud/network/vpn/RemoteAccessVpnManagerImpl.java b/server/src/main/java/com/cloud/network/vpn/RemoteAccessVpnManagerImpl.java index 2030a5a4ee55..91079e0f8746 100644 --- a/server/src/main/java/com/cloud/network/vpn/RemoteAccessVpnManagerImpl.java +++ b/server/src/main/java/com/cloud/network/vpn/RemoteAccessVpnManagerImpl.java @@ -24,17 +24,23 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import org.apache.commons.collections.CollectionUtils; import org.apache.log4j.Logger; import org.apache.cloudstack.acl.SecurityChecker.AccessType; +import org.apache.cloudstack.api.ResourceDetail; import org.apache.cloudstack.api.command.user.vpn.ListRemoteAccessVpnsCmd; import org.apache.cloudstack.api.command.user.vpn.ListVpnUsersCmd; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.pki.PkiDetail; +import org.apache.cloudstack.pki.PkiManager; +import org.apache.cloudstack.resourcedetail.dao.RemoteAccessVpnDetailsDao; import com.cloud.configuration.Config; +import com.cloud.domain.Domain; import com.cloud.domain.DomainVO; import com.cloud.domain.dao.DomainDao; import com.cloud.event.ActionEvent; @@ -44,6 +50,7 @@ import com.cloud.exception.AccountLimitException; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.RemoteAccessVpnException; import com.cloud.exception.ResourceUnavailableException; import com.cloud.network.Network; import com.cloud.network.Network.Service; @@ -90,13 +97,14 @@ import com.cloud.utils.db.TransactionCallbackWithException; import com.cloud.utils.db.TransactionStatus; import com.cloud.utils.net.NetUtils; -import org.apache.commons.collections.CollectionUtils; public class RemoteAccessVpnManagerImpl extends ManagerBase implements RemoteAccessVpnService, Configurable { private final static Logger s_logger = Logger.getLogger(RemoteAccessVpnManagerImpl.class); + static final ConfigKey RemoteAccessVpnType = new ConfigKey("Network", String.class, RemoteAccessVpnTypeConfigKey, "ikev2", "Type of VPN (ikev2 or l2tp)", false, + ConfigKey.Scope.Account); static final ConfigKey RemoteAccessVpnClientIpRange = new ConfigKey("Network", String.class, RemoteAccessVpnClientIpRangeCK, "10.1.2.1-10.1.2.8", - "The range of ips to be allocated to remote access vpn clients. The first ip in the range is used by the VPN server", false, ConfigKey.Scope.Account); + "The range of ips to be allocated to remote access vpn clients. The first ip in the range is used by the VPN server", false, ConfigKey.Scope.Account); @Inject AccountDao _accountDao; @@ -105,6 +113,8 @@ public class RemoteAccessVpnManagerImpl extends ManagerBase implements RemoteAcc @Inject RemoteAccessVpnDao _remoteAccessVpnDao; @Inject + RemoteAccessVpnDetailsDao _remoteAccessVpnDetailsDao; + @Inject IPAddressDao _ipAddressDao; @Inject AccountManager _accountMgr; @@ -131,13 +141,16 @@ public class RemoteAccessVpnManagerImpl extends ManagerBase implements RemoteAcc @Inject VpcDao _vpcDao; + @Inject + private PkiManager pkiManager; + int _userLimit; int _pskLength; SearchBuilder VpnSearch; @Override @DB - public RemoteAccessVpn createRemoteAccessVpn(final long publicIpId, String ipRange, boolean openFirewall, final Boolean forDisplay) throws NetworkRuleConflictException { + public RemoteAccessVpn createRemoteAccessVpn(final long publicIpId, String ipRange, boolean openFirewall, final Boolean forDisplay) throws NetworkRuleConflictException, RemoteAccessVpnException { CallContext ctx = CallContext.current(); final Account caller = ctx.getCallingAccount(); @@ -235,25 +248,81 @@ public RemoteAccessVpn createRemoteAccessVpn(final long publicIpId, String ipRan long startIp = NetUtils.ip2Long(range[0]); final String newIpRange = NetUtils.long2Ip(++startIp) + "-" + range[1]; - final String sharedSecret = PasswordGenerator.generatePresharedKey(_pskLength); + final String vpnType = RemoteAccessVpnType.value(); + + // use server.secret.pem instead of pre-shared key for VPN IKEv2 + final String sharedSecret = vpnType.equalsIgnoreCase(RemoteAccessVpnService.Type.IKEV2.toString()) ? null : PasswordGenerator.generatePresharedKey(_pskLength); - return Transaction.execute(new TransactionCallbackWithException() { + RemoteAccessVpn vpn = Transaction.execute(new TransactionCallbackWithException() { @Override public RemoteAccessVpn doInTransaction(TransactionStatus status) throws NetworkRuleConflictException { if (vpcId == null) { _rulesMgr.reservePorts(ipAddr, NetUtils.UDP_PROTO, Purpose.Vpn, openFirewallFinal, caller, NetUtils.VPN_PORT, NetUtils.VPN_L2TP_PORT, NetUtils.VPN_NATT_PORT); } - RemoteAccessVpnVO vpnVO = - new RemoteAccessVpnVO(ipAddr.getAccountId(), ipAddr.getDomainId(), ipAddr.getAssociatedWithNetworkId(), publicIpId, vpcId, range[0], newIpRange, - sharedSecret); + RemoteAccessVpnVO vpnVO = new RemoteAccessVpnVO( + ipAddr.getAccountId(), + ipAddr.getDomainId(), + ipAddr.getAssociatedWithNetworkId(), + publicIpId, + vpcId, + range[0], + newIpRange, + sharedSecret, + vpnType); if (forDisplay != null) { vpnVO.setDisplay(forDisplay); } + return _remoteAccessVpnDao.persist(vpnVO); } }); + + if (vpnType.equalsIgnoreCase(RemoteAccessVpnService.Type.IKEV2.toString())) { + try { + // issue a signed certificate for the public IP through Vault + final Domain domain = _domainMgr.findDomainByIdOrPath(ipAddr.getDomainId(), null); + final PkiDetail credential = pkiManager.issueCertificate(domain, ipAddress.getAddress()); + + Transaction.execute(new TransactionCallback() { + @Override + public ResourceDetail doInTransaction(TransactionStatus status) throws RuntimeException { + // note that all the vpn details will be encrypted and then stored in database + _remoteAccessVpnDetailsDao.addDetail(vpn.getId(), PkiManager.CREDENTIAL_ISSUING_CA, credential.getIssuingCa(), false); + _remoteAccessVpnDetailsDao.addDetail(vpn.getId(), PkiManager.CREDENTIAL_SERIAL_NUMBER, credential.getSerialNumber(), false); + _remoteAccessVpnDetailsDao.addDetail(vpn.getId(), PkiManager.CREDENTIAL_CERTIFICATE, credential.getCertificate(), false); + _remoteAccessVpnDetailsDao.addDetail(vpn.getId(), PkiManager.CREDENTIAL_PRIVATE_KEY, credential.getPrivateKey(), false); + + // no need to return anything here + return null; + } + }); + + Transaction.execute(new TransactionCallback() { + @Override + public Boolean doInTransaction(TransactionStatus status) { + RemoteAccessVpnVO vpnVO = (RemoteAccessVpnVO)vpn; + + vpnVO.setCaCertificate(credential.getIssuingCa()); + + return _remoteAccessVpnDao.update(vpnVO.getId(), vpnVO); + } + }); + } catch (RemoteAccessVpnException | RuntimeException e) { + // clean up just created vpn + Transaction.execute(new TransactionCallback() { + @Override + public Boolean doInTransaction(TransactionStatus status) { + return _remoteAccessVpnDao.remove(vpn.getId()); + } + }); + + throw e; + } + } + + return vpn; } private void validateRemoteAccessVpnConfiguration() throws ConfigurationException { @@ -755,7 +824,7 @@ public String getConfigComponentName() { @Override public ConfigKey[] getConfigKeys() { - return new ConfigKey[] {RemoteAccessVpnClientIpRange}; + return new ConfigKey[] {RemoteAccessVpnType, RemoteAccessVpnClientIpRange}; } public List getVpnServiceProviders() { diff --git a/server/src/main/java/org/apache/cloudstack/pki/PkiConfig.java b/server/src/main/java/org/apache/cloudstack/pki/PkiConfig.java new file mode 100644 index 000000000000..35984482a61c --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/pki/PkiConfig.java @@ -0,0 +1,120 @@ +// 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.pki; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.cloudstack.framework.config.ConfigKey; + +/** + * @author Khosrow Moossavi + * @since 4.12.0.0 + */ +public enum PkiConfig { + CertificateBrand("Network", String.class, "pki.engine.certificate.brand", "CloudStack", "Brand name to be used in Certificate's common name"), + CertificateCommonName("Network", String.class, "pki.engine.certificate.common.name", "__BRAND__ VPN __DOMAIN__ CA", + "Certificate's common name template (brand will be filled from 'pki.engine.certificate.brand', domain will be provided on the fly"), + VaultEnabled("Network", Boolean.class, "pki.engine.vault.enabled", "false", "Enable Vault as the backend PKI engine"), + VaultUrl("Network", String.class, "pki.engine.vault.url", "", "Full URL of Vault endpoint (e.g. http://127.0.0.1:8200)"), + VaultToken("Network", String.class, "pki.engine.vault.token", "", "Token to access Vault"), + VaultAppRoleId("Network", String.class, "pki.engine.vault.token.role.id", "", "App Role id to be used to fetch token to access Vault"), + VaultAppSecretId("Network", String.class, "pki.engine.vault.token.secret.id", "", "Secret id to be used to fetch token to access Vault"), + VaultPkiTtl("Network", String.class, "pki.engine.vault.ttl", "87600h", "Vault PKI TTL (e.g. 87600h)"), + VaultCATtl("Network", String.class, "pki.engine.vault.cca.ttl", "87600h", "Vault PKI root CA TTL (e.g. 87600h)"), + VaultRoleName("Network", String.class, "pki.engine.vault.role.name", "cloudstack-vpn", "Vault PKI role name"), + VaultRoleTtl("Network", String.class, "pki.engine.vault.role.ttl", "43800h", "Vault PKI role TTL (e.g. 43800h)"), + VaultMounthPath("Network", String.class, "pki.engine.vault.mount.path", "pki/cloudstack", "Vault PKI mount point prefix (must not end with trailing slash)"); + + private final String _category; + private final Class _type; + private final String _name; + private final String _defaultValue; + private final String _description; + private final boolean _dynamic; + private final ConfigKey.Scope _scope; + + private static final List PkiEngineConfigKeys = new ArrayList(); + + static { + Arrays.stream(PkiConfig.values()).forEach(c -> PkiEngineConfigKeys.add(c.key())); + } + + private PkiConfig(String category, Class type, String name, String defaultValue, String description) { + _category = category; + _type = type; + _name = name; + _defaultValue = defaultValue; + _description = description; + _dynamic = false; + _scope = ConfigKey.Scope.Global; + } + + public String getCategory() { + return _category; + } + + public Class getType() { + return _type; + } + + public String getName() { + return _name; + } + + public String getDefaultValue() { + return _defaultValue; + } + + public String getDescription() { + return _description; + } + + public boolean isDynamic() { + return _dynamic; + } + + public ConfigKey.Scope getScope() { + return _scope; + } + + public String key() { + return _name; + } + + public static boolean doesKeyExist(String key) { + return PkiEngineConfigKeys.contains(key); + } + + public static ConfigKey[] asConfigKeys() { + return Arrays.stream(PkiConfig.values()) + .map(config -> asConfigKey(config)) + .toArray(ConfigKey[]::new); + } + + public static ConfigKey asConfigKey(PkiConfig config) { + return new ConfigKey<>( + config.getCategory(), + config.getType(), + config.getName(), + config.getDefaultValue(), + config.getDescription(), + config.isDynamic(), + config.getScope()); + } +} diff --git a/server/src/main/java/org/apache/cloudstack/pki/PkiEngine.java b/server/src/main/java/org/apache/cloudstack/pki/PkiEngine.java new file mode 100644 index 000000000000..4e6ad0b5ae2b --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/pki/PkiEngine.java @@ -0,0 +1,52 @@ +// 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.pki; + +import com.cloud.domain.Domain; +import com.cloud.utils.net.Ip; + +/** + * @author Khosrow Moossavi + * @since 4.12.0.0 + */ +public interface PkiEngine { + /** + * Issue a Certificate for specific IP and specific Domain act as the CA. This will have two + * known implementation, {@link PkiEngineDefault} and {@link PkiEngineVault}. Vault implementation + * will delegate everything CA-related to Vault to process it, while Default will assume the + * CA-related actions will be done within the scope of the same application. + * + * @param domain object to extract name and id to be used to issuing CA + * @param publicIp to be included in the certificate + * + * @return details about the just signed PKI, including issuing CA, certificate, private key and serial number + * + * @throws Exception + */ + PkiDetail issueCertificate(Domain domain, Ip publicIp) throws Exception; + + /** + * Get a Certificate for specific Domain act as the CA + * + * @param domain object to extract its id to be find the issuing CA + * + * @return details about signed PKI, including issuing CA, certificate and serial number + * + * @throws Exception + */ + PkiDetail getCertificate(Domain domain) throws Exception; +} diff --git a/server/src/main/java/org/apache/cloudstack/pki/PkiEngineDefault.java b/server/src/main/java/org/apache/cloudstack/pki/PkiEngineDefault.java new file mode 100644 index 000000000000..88d8da593762 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/pki/PkiEngineDefault.java @@ -0,0 +1,47 @@ +// 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.pki; + +import java.util.Map; + +import com.cloud.domain.Domain; +import com.cloud.utils.net.Ip; + +/** + * @author Khosrow Moossavi + * @since 4.12.0.0 + */ +public class PkiEngineDefault implements PkiEngine { + public PkiEngineDefault(Map configs) { + } + + /* (non-Javadoc) + * @see org.apache.cloudstack.pki.PkiEngine#issueCertificate(com.cloud.domain.Domain, com.cloud.utils.net.Ip) + */ + @Override + public PkiDetail issueCertificate(Domain domain, Ip publicIp) throws Exception { + throw new UnsupportedOperationException("Cannot issue certificate with Default implementation, use Vault instead."); + } + + /* (non-Javadoc) + * @see org.apache.cloudstack.pki.PkiEngine#getCertificate(com.cloud.domain.Domain) + */ + @Override + public PkiDetail getCertificate(Domain domain) { + throw new UnsupportedOperationException("Cannot get certificate with Default implementation, use Vault instead."); + } +} diff --git a/server/src/main/java/org/apache/cloudstack/pki/PkiEngineVault.java b/server/src/main/java/org/apache/cloudstack/pki/PkiEngineVault.java new file mode 100644 index 000000000000..3769e967a604 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/pki/PkiEngineVault.java @@ -0,0 +1,329 @@ +// 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.pki; + +import java.util.Arrays; +import java.util.Map; + +import org.apache.commons.lang.StringUtils; +import org.springframework.util.Assert; + +import com.bettercloud.vault.Vault; +import com.bettercloud.vault.VaultConfig; +import com.bettercloud.vault.VaultException; +import com.bettercloud.vault.api.Logical; +import com.bettercloud.vault.api.pki.Credential; +import com.bettercloud.vault.api.pki.Pki; +import com.bettercloud.vault.api.pki.RoleOptions; +import com.bettercloud.vault.response.AuthResponse; +import com.bettercloud.vault.response.LogicalResponse; +import com.bettercloud.vault.response.PkiResponse; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; + +import com.cloud.domain.Domain; +import com.cloud.utils.net.Ip; + +/** + * @author Khosrow Moossavi + * @since 4.12.0.0 + */ +public class PkiEngineVault implements PkiEngine { + public static final int RETRY_COUNT = 2; + public static final int RETRY_INTERVAL_MILISECONDS = 2000; + public static final int OPEN_CONNECTION_TIMEOUT_SECONDS = 5; + public static final int READ_CONNECTION_TIMEOUT_SECONDS = 5; + + private final String _vaultUrl; + private final String _vaultToken; + private final String _vaultTokenRoleId; + private final String _vaultTokenSecretId; + private final String _vaultRoleName; + private final String _vaultMountPath; + + private final String _certificateCommonName; + private final String _vaultPkiTtl; + private final String _vaultCATtl; + private final String _vaultRoleTtl; + + public PkiEngineVault(Map configs) { + _vaultUrl = configs.get(PkiConfig.VaultUrl.key()); + Assert.isTrue(!Strings.isNullOrEmpty(_vaultUrl), "PKI Engine: URL of Vault endpoint is missing"); + + _vaultToken = configs.get(PkiConfig.VaultToken.key()); + + // if Token provided ignore RoleId and SecretId + if (!Strings.isNullOrEmpty(_vaultToken)) { + _vaultTokenRoleId = null; + _vaultTokenSecretId = null; + } else { + _vaultTokenRoleId = configs.get(PkiConfig.VaultAppRoleId.key()); + _vaultTokenSecretId = configs.get(PkiConfig.VaultAppSecretId.key()); + + if (Strings.isNullOrEmpty(_vaultTokenRoleId) && Strings.isNullOrEmpty(_vaultTokenSecretId)) { + throw new IllegalArgumentException("PKI Engine: Vault Token access and RoleId and SecretId are missing"); + } + } + + _vaultRoleName = configs.get(PkiConfig.VaultRoleName.key()); + Assert.isTrue(!Strings.isNullOrEmpty(_vaultRoleName), "PKI Engine: Vault PKI role name is missing"); + + String mountPath = configs.get(PkiConfig.VaultMounthPath.key()); + + Assert.isTrue(!Strings.isNullOrEmpty(mountPath), "PKI Engine: Vault PKI mount path is missing"); + Assert.isTrue(!StringUtils.endsWith(mountPath, "/"), "PKI Engine: Vault PKI mount path must not end with trailing slash, current value: " + mountPath); + + _vaultMountPath = mountPath + "/%s"; + + String certificateBrand = configs.get(PkiConfig.CertificateBrand.key()); + _certificateCommonName = configs.get(PkiConfig.CertificateCommonName.key()).replaceAll("__BRAND__", certificateBrand); + + _vaultPkiTtl = configs.get(PkiConfig.VaultPkiTtl.key()); + Assert.isTrue(!Strings.isNullOrEmpty(_vaultPkiTtl), "PKI Engine: Vault PKI TTL is missing"); + + _vaultCATtl = configs.get(PkiConfig.VaultCATtl.key()); + Assert.isTrue(!Strings.isNullOrEmpty(_vaultCATtl), "PKI Engine: Vault PKI root CA TTL is missing"); + + _vaultRoleTtl = configs.get(PkiConfig.VaultRoleTtl.key()); + Assert.isTrue(!Strings.isNullOrEmpty(_vaultRoleTtl), "PKI Engine: Vault PKI role TTL is missing"); + } + + /* (non-Javadoc) + * @see org.apache.cloudstack.pki.PkiEngine#issueCertificate(com.cloud.domain.Domain, com.cloud.utils.net.Ip) + */ + @Override + public PkiDetail issueCertificate(Domain domain, Ip publicIp) throws VaultException { + Assert.notNull(domain, "PKI Engine: Cannot issue Certificate because domain is null"); + + Vault vault = new VaultBuilder().build(); + + createRoleIfMissing(vault, domain); + + final String path = String.format(_vaultMountPath, domain.getUuid()); + Pki pki = vault.pki(path); + + PkiResponse response = pki.issue(_vaultRoleName, publicIp.addr(), null, Arrays.asList(publicIp.addr()), null, null); + Credential credential = response.getCredential(); + + if (response.getRestResponse().getStatus() == 404) { + throw new VaultException("Cannot find Vault PKI backend path for domain " + domain.getUuid()); + } + + return new PkiDetail() + .certificate(credential.getCertificate()) + .issuingCa(credential.getIssuingCa()) + .privateKey(credential.getPrivateKey()) + .privateKeyType(credential.getPrivateKeyType()) + .serialNumber(credential.getSerialNumber()); + } + + /* (non-Javadoc) + * @see org.apache.cloudstack.pki.PkiEngine#getCertificate(com.cloud.domain.Domain) + */ + @Override + public PkiDetail getCertificate(Domain domain) throws VaultException { + Assert.notNull(domain, "PKI Engine: Cannot get Certificate because domain is null"); + + Vault vault = new VaultBuilder().build(); + Logical logical = vault.logical(); + + final String path = String.format(_vaultMountPath, domain.getUuid()); + final String apiEndoint = new StringBuilder() + .append(path) + .append("/cert/ca") + .toString(); + + LogicalResponse response = logical.read(apiEndoint); + Map data = response.getData(); + + Assert.hasLength(data.get("certificate"), "PKI Engine: Cannot get Certificate, Vault response is empty"); + + return new PkiDetail().issuingCa(data.get("certificate")); + } + + /** + * Create Vault PKI role if it's missing or return the existing one + * + * @param vault object + * @param domain object + * + * @return newly created or existing Vault PKI role + * + * @throws VaultException + */ + private RoleOptions createRoleIfMissing(Vault vault, Domain domain) throws VaultException { + final String path = String.format(_vaultMountPath, domain.getUuid()); + Pki pki = vault.pki(path); + PkiResponse response = pki.getRole(_vaultRoleName); + RoleOptions role = response.getRoleOptions(); + + // role does exist + if (response.getRestResponse().getStatus() == 200) { + return role; + } + + createMountPointIfMissing(vault, domain); + createRootCertIfMissing(vault, domain); + createConfigUrlIfMissing(vault, domain); + + // create new role + RoleOptions options = new RoleOptions() + .allowAnyName(true) + .ttl(_vaultRoleTtl); + + return pki.createOrUpdateRole(_vaultRoleName, options).getRoleOptions(); + } + + /** + * Create Vault PKI engine mount point if it's missing + * + * @param vault object + * @param domain object + * + * @throws VaultException + */ + private void createMountPointIfMissing(Vault vault, Domain domain) throws VaultException { + final String sysMountBase = "sys/mounts"; + final String path = String.format(_vaultMountPath, domain.getUuid()); + final String apiEndpoint = new StringBuilder() + .append(sysMountBase) + .append("/") + .append(path) + .toString(); + + try { + vault.logical().read(sysMountBase + "/tune"); + return; + } catch (VaultException e) { + // mount point not found, continue to create it + } + + // create mount point + Map createPayload = ImmutableMap.of("type", "pki"); + vault.logical().write(apiEndpoint, createPayload); + + // tune mount point + Map tunePayload = ImmutableMap.of( + "default_lease_ttl", _vaultPkiTtl, + "max_lease_ttl", _vaultPkiTtl, + "description", domain.getName()); + vault.logical().write(apiEndpoint + "/tune", tunePayload); + } + + /** + * Create Vault root Certificate CA if it's missing + * + * @param vault object + * @param domain object + * + * @throws VaultException + */ + private void createRootCertIfMissing(Vault vault, Domain domain) throws VaultException { + String path = String.format(_vaultMountPath, domain.getUuid()); + final String apiEndpoint = new StringBuilder() + .append(path) + .append("/root/generate/internal") + .toString(); + + final String commonName = _certificateCommonName.replaceAll("__DOMAIN__", domain.getName()); + Map payload = ImmutableMap.of("common_name", commonName, "ttl", _vaultCATtl); + + vault.logical().write(apiEndpoint, payload); + } + + /** + * create Vault PKI CRL config URLs if they are missing + * + * @param vault object + * @param domain object + * + * @throws VaultException + */ + private void createConfigUrlIfMissing(Vault vault, Domain domain) throws VaultException { + final String path = String.format(_vaultMountPath, domain.getUuid()); + final String apiEndpoint = new StringBuilder() + .append(path) + .append("/config/urls") + .toString(); + + try { + vault.logical().read(apiEndpoint); + return; + } catch (VaultException e) { + // config urls for this pki endpoint don't exist, continue to create them + } + + String caUrl = new StringBuilder() + .append(_vaultUrl) + .append("/v1/") + .append(path) + .append("/ca") + .toString(); + + String crlUrl = new StringBuilder() + .append(_vaultUrl) + .append("/v1/") + .append(path) + .append("/crl") + .toString(); + + // create CRL config urls + Map createPayload = ImmutableMap.of("issuing_certificates", caUrl, "crl_distribution_points", crlUrl); + vault.logical().write(apiEndpoint, createPayload); + } + + /** + * Vault object builder + */ + private class VaultBuilder { + private VaultBuilder() { + } + + /** + * Build Vault object based on provided information and scenarios + * + * 1) Vault Token is provided: create VaultConfig and Vault object right away + * 2) Vault Token is not provided: fetching Vault Token based on provided RoleId and SecretId + * + * @return Vault object containing client token (provided or fetched) + * + * @throws VaultException + */ + public Vault build() throws VaultException { + final VaultConfig config = new VaultConfig() + .address(_vaultUrl) + .token(_vaultToken) + .openTimeout(OPEN_CONNECTION_TIMEOUT_SECONDS) + .readTimeout(READ_CONNECTION_TIMEOUT_SECONDS) + .build(); + + // Vault Token is provided, Vault object can be initialized right away + if (!Strings.isNullOrEmpty(_vaultToken)) { + return new Vault(config).withRetries(RETRY_COUNT, RETRY_INTERVAL_MILISECONDS); + } + + // Vault Token is not provided, but AppRole information is. + // We're going to fetch client token through REST API call. + AuthResponse response = new Vault(config).auth().loginByAppRole(_vaultTokenRoleId, _vaultTokenSecretId); + + // putting back client token on VaultConfig for further use + config.token(response.getAuthClientToken()); + + return new Vault(config).withRetries(RETRY_COUNT, RETRY_INTERVAL_MILISECONDS); + } + } +} diff --git a/server/src/main/java/org/apache/cloudstack/pki/PkiManagerImpl.java b/server/src/main/java/org/apache/cloudstack/pki/PkiManagerImpl.java new file mode 100644 index 000000000000..8cef49434501 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/pki/PkiManagerImpl.java @@ -0,0 +1,103 @@ +// 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.pki; + +import java.util.Map; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; + +import org.apache.commons.lang.BooleanUtils; + +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; + +import com.cloud.domain.Domain; +import com.cloud.exception.RemoteAccessVpnException; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.net.Ip; + +/** + * PKI Manager base class. This will work as a factory to construct Vault or Default + * implementation and pass through the API call to corresponding implementation. + * + * @author Khosrow Moossavi + * @since 4.12.0.0 + */ +public class PkiManagerImpl extends ManagerBase implements PkiManager, Configurable { + @Inject + private ConfigurationDao configDao; + + private PkiEngine pkiEngine; + + /* (non-Javadoc) + * @see com.cloud.utils.component.ComponentLifecycleBase#configure(java.lang.String, java.util.Map) + */ + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + Map configs = configDao.getConfiguration(params); + + if (BooleanUtils.toBoolean(configs.get(PkiConfig.VaultEnabled.key()))) { + pkiEngine = new PkiEngineVault(configs); + } else { + pkiEngine = new PkiEngineDefault(configs); + } + + return true; + } + + /* (non-Javadoc) + * @see org.apache.cloudstack.framework.config.Configurable#getConfigComponentName() + */ + @Override + public String getConfigComponentName() { + return PkiManager.class.getSimpleName(); + } + + /* (non-Javadoc) + * @see org.apache.cloudstack.framework.config.Configurable#getConfigKeys() + */ + @Override + public ConfigKey[] getConfigKeys() { + return PkiConfig.asConfigKeys(); + } + + /* (non-Javadoc) + * @see org.apache.cloudstack.pki.PkiManager#issueCertificate(com.cloud.domain.Domain, com.cloud.utils.net.Ip) + */ + @Override + public PkiDetail issueCertificate(Domain domain, Ip publicIp) throws RemoteAccessVpnException { + try { + return pkiEngine.issueCertificate(domain, publicIp); + } catch (Exception e) { + throw new RemoteAccessVpnException(e.getMessage()); + } + } + + /* (non-Javadoc) + * @see org.apache.cloudstack.pki.PkiManager#getCertificate(com.cloud.domain.Domain) + */ + @Override + public PkiDetail getCertificate(Domain domain) throws RemoteAccessVpnException { + try { + return pkiEngine.getCertificate(domain); + } catch (Exception e) { + throw new RemoteAccessVpnException(e.getMessage()); + } + } +} diff --git a/systemvm/debian/etc/ipsec.d/ikev2.conf b/systemvm/debian/etc/ipsec.d/ikev2.conf new file mode 100644 index 000000000000..6bb449901b7d --- /dev/null +++ b/systemvm/debian/etc/ipsec.d/ikev2.conf @@ -0,0 +1,26 @@ +#ipsec remote access vpn with IKEv2 configuration +config setup + plutostart=no + +conn IKEv2-Remote + dpdaction=clear + rekey=no + reauth=no + keyexchange=ikev2 + + leftauth=pubkey + left=172.26.10.151 + leftid=172.26.10.151 + leftcert=server.cert.pem + leftsendcert=always + leftsubnet=10.153.252.0/24,10.153.253.0/24 + leftfirewall=yes + + right=%any + rightsourceip=10.1.2.0/24 + rightauth=eap-mschapv2 + rightsendcert=never # see note + + eap_identity=%any + + auto=add diff --git a/systemvm/debian/opt/cloud/bin/configure.py b/systemvm/debian/opt/cloud/bin/configure.py index be67f403c8be..e6c6ed404c0a 100755 --- a/systemvm/debian/opt/cloud/bin/configure.py +++ b/systemvm/debian/opt/cloud/bin/configure.py @@ -611,17 +611,32 @@ def convert_sec_to_h(self, val): class CsVpnUser(CsDataBag): PPP_CHAP = '/etc/ppp/chap-secrets' + IKEV2_SECRETS='/etc/ipsec.d/ipsec.any.secrets' def process(self): + vpn_type = self.dbag['vpn_type'] for user in self.dbag: if user == 'id': continue + elif user == 'vpn_type': + continue userconfig = self.dbag[user] if userconfig['add']: - self.add_l2tp_ipsec_user(user, userconfig) + if vpn_type == "ikev2": + self.add_ikev2_ipsec_user(user, userconfig) + elif vpn_type == "l2tp": + self.add_l2tp_ipsec_user(user, userconfig) else: - self.del_l2tp_ipsec_user(user, userconfig) + if vpn_type == "ikev2": + self.del_ikev2_ipsec_user(user, userconfig) + elif vpn_type == "l2tp": + self.del_l2tp_ipsec_user(user, userconfig) + + if vpn_type == "ikev2": + CsHelper.execute("service ipsec start") + CsHelper.execute("ipsec update") + CsHelper.execute("ipsec rereadsecrets") def add_l2tp_ipsec_user(self, user, obj): userfound = False @@ -638,6 +653,28 @@ def add_l2tp_ipsec_user(self, user, obj): file.add(userAddEntry) file.commit() + def add_ikev2_ipsec_user(self, user, obj): + userfound = False + password = obj['password'] + + rsaEntry = ": RSA server.key.pem" + userAddEntry = "%s : EAP \"%s\"" %(user,password) + logging.debug("Adding vpn user '%s'" % user) + + file = CsFile(self.IKEV2_SECRETS) + + rsafound = file.searchString(rsaEntry, '#') + if not rsafound: + file.append(rsaEntry, 0) + + userfound = file.searchString(userAddEntry, '#') + if not userfound: + logging.debug("User is not there already, so adding user") + self.del_ikev2_ipsec_user(user, obj) + file.add(userAddEntry) + + file.commit() + def del_l2tp_ipsec_user(self, user, obj): userfound = False password = obj['password'] @@ -665,6 +702,19 @@ def del_l2tp_ipsec_user(self, user, obj): logging.debug("killing process %s" % pid) CsHelper.execute('kill -9 %s' % pid) + def del_ikev2_ipsec_user(self, user, obj): + userfound = False + password = obj['password'] + userentry = "%s : EAP \"%s\"" % (user,password) + + logging.debug("Deleting the user '%s'" % user) + file = CsFile(self.IKEV2_SECRETS) + file.deleteLine(userentry) + file.commit() + + establishedid = CsHelper.execute("ipsec statusall | grep '%s' | awk '{print $1}' | sed 's/://g'" % user) + if len(establishedid) > 0: + CsHelper.execute("ipsec down %s" % establishedid[0]) class CsRemoteAccessVpn(CsDataBag): VPNCONFDIR = "/etc/ipsec.d" @@ -674,6 +724,14 @@ def process(self): logging.debug(self.dbag) + l2tpconffile="%s/l2tp.conf" % (self.VPNCONFDIR) + if os.path.exists(l2tpconffile): + os.rename(l2tpconffile, l2tpconffile + "-disabled") + + ikev2conffile="%s/ikev2.conf" % (self.VPNCONFDIR) + if os.path.exists(ikev2conffile): + os.rename(ikev2conffile, ikev2conffile + "-disabled") + for public_ip in self.dbag: if public_ip == "id": continue @@ -683,18 +741,40 @@ def process(self): if vpnconfig['create']: logging.debug("Enabling remote access vpn on " + public_ip) - CsHelper.start_if_stopped("ipsec") - self.configure_l2tpIpsec(public_ip, self.dbag[public_ip]) - logging.debug("Remote accessvpn data bag %s", self.dbag) - self.remoteaccessvpn_iptables(public_ip, self.dbag[public_ip]) - - CsHelper.execute("ipsec update") - CsHelper.execute("systemctl start xl2tpd") - CsHelper.execute("ipsec rereadsecrets") + if vpnconfig["vpn_type"] == "ikev2": + CsHelper.start_if_stopped("ipsec") + self.configure_ikev2Ipsec(public_ip, self.dbag[public_ip]) + logging.debug("Remote accessvpn data bag %s", self.dbag) + self.remoteaccessvpn_iptables(public_ip, self.dbag[public_ip]) + + CsHelper.execute("service ipsec start") + CsHelper.execute("ipsec update") + CsHelper.execute("ipsec rereadsecrets") + + elif vpnconfig["vpn_type"] == "l2tp": + CsHelper.start_if_stopped("ipsec") + self.configure_l2tpIpsec(public_ip, self.dbag[public_ip]) + logging.debug("Remote accessvpn data bag %s", self.dbag) + self.remoteaccessvpn_iptables(public_ip, self.dbag[public_ip]) + + CsHelper.execute("ipsec update") + CsHelper.execute("service xl2tpd start") + CsHelper.execute("ipsec rereadsecrets") else: logging.debug("Disabling remote access vpn .....") - CsHelper.execute("ipsec down L2TP-PSK") - CsHelper.execute("systemctl stop xl2tpd") + if vpnconfig["vpn_type"] == "ikev2": + if not os.path.exists(ikev2conffile): + os.rename(ikev2conffile + "-disabled", ikev2conffile) + + CsHelper.execute("ipsec down IKEv2-Remote") + CsHelper.execute("service ipsec stop") + + elif vpnconfig["vpn_type"] == "l2tp": + if not os.path.exists(l2tpconffile): + os.rename(l2tpconffile + "-disabled", l2tpconffile) + + CsHelper.execute("ipsec down L2TP-PSK") + CsHelper.execute("service xl2tpd stop") def configure_l2tpIpsec(self, left, obj): l2tpconffile = "%s/l2tp.conf" % (self.VPNCONFDIR) @@ -702,6 +782,9 @@ def configure_l2tpIpsec(self, left, obj): xl2tpdconffile = "/etc/xl2tpd/xl2tpd.conf" xl2tpoptionsfile = "/etc/ppp/options.xl2tpd" + if not os.path.exists(l2tpconffile): + os.rename(l2tpconffile + "-disabled", l2tpconffile) + localip = obj['local_ip'] localcidr = obj['local_cidr'] publicIface = obj['public_interface'] @@ -727,6 +810,50 @@ def configure_l2tpIpsec(self, left, obj): xl2tpoptions.search("ms-dns ", "ms-dns %s" % localip) xl2tpoptions.commit() + def configure_ikev2Ipsec(self, left, obj): + ikev2conffile="%s/ikev2.conf" % (self.VPNCONFDIR) + vpnsecretfilte="%s/ipsec.any.secrets" % (self.VPNCONFDIR) + + cacertfilte="%s/cacerts/ca.cert.pem" % (self.VPNCONFDIR) + servercertfilte="%s/certs/server.cert.pem" % (self.VPNCONFDIR) + serverkeyfilte="%s/private/server.key.pem" % (self.VPNCONFDIR) + + localip=obj['local_ip'] + localcidr=obj['local_cidr'] + publicIface=obj['public_interface'] + iprange=obj['ip_range'] + cacert=obj['ca_cert'] + servercert=obj['server_cert'] + serverkey=obj['server_key'] + + if not os.path.exists(ikev2conffile): + os.rename(ikev2conffile + "-disabled", ikev2conffile) + + # updating 'left' detail in ikev2-remote.conf + file = CsFile(ikev2conffile) + file.addeq(" left=%s" % left) + file.addeq(" leftid=%s" % left) + file.addeq(" leftsubnet=%s" % localcidr) + file.commit() + + # CA Cert + file = CsFile(cacertfilte) + file.empty() + file.addeq(cacert) + file.commit() + + # Server Cert + file = CsFile(servercertfilte) + file.empty() + file.addeq(servercert) + file.commit() + + # Server Key + file = CsFile(serverkeyfilte) + file.empty() + file.addeq(serverkey) + file.commit() + def remoteaccessvpn_iptables(self, publicip, obj): publicdev = obj['public_interface'] localcidr = obj['local_cidr'] diff --git a/systemvm/debian/opt/cloud/bin/cs_remoteaccessvpn.py b/systemvm/debian/opt/cloud/bin/cs_remoteaccessvpn.py index dff05bd28145..72fbfba2470a 100755 --- a/systemvm/debian/opt/cloud/bin/cs_remoteaccessvpn.py +++ b/systemvm/debian/opt/cloud/bin/cs_remoteaccessvpn.py @@ -19,9 +19,5 @@ def merge(dbag, vpn): key = vpn['vpn_server_ip'] - op = vpn['create'] - if key in dbag.keys() and not op: - del(dbag[key]) - else: - dbag[key] = vpn + dbag[key] = vpn return dbag diff --git a/systemvm/debian/opt/cloud/bin/cs_vpnusers.py b/systemvm/debian/opt/cloud/bin/cs_vpnusers.py index 3bef1fec239a..3d1fe7eb379b 100755 --- a/systemvm/debian/opt/cloud/bin/cs_vpnusers.py +++ b/systemvm/debian/opt/cloud/bin/cs_vpnusers.py @@ -31,11 +31,15 @@ def merge(dbag, data): for user in dbagc.keys(): if user == 'id': continue + elif user == 'vpn_type': + continue userrec = dbagc[user] add = userrec['add'] if not add: del(dbagc[user]) + dbagc['vpn_type'] = data["vpn_type"] + for user in data['vpn_users']: username = user['user'] add = user['add']