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 extends VpnUser> 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']