Example #1
0
        /// <inheritdoc/>
        public override void Run(CommandLine commandLine)
        {
            if (commandLine.Arguments.Length < 1)
            {
                Console.Error.WriteLine("*** ERROR: HIVE-DEF is required.");
                Program.Exit(1);
            }

            // Parse and validate the hive definition.

            HiveDefinition.FromFile(commandLine.Arguments[0], strict: true);

            Console.WriteLine("");
            Console.WriteLine("*** The hive definition is OK.");
        }
Example #2
0
        /// <inheritdoc/>
        public override void Run(CommandLine commandLine)
        {
            if (commandLine.HasHelpOption)
            {
                Help();
                Program.Exit(0);
            }

            // Special-case handling of the [--remove-templates] option.

            if (commandLine.HasOption("--remove-templates"))
            {
                Console.WriteLine("Removing cached virtual machine templates.");

                foreach (var fileName in Directory.GetFiles(HiveHelper.GetVmTemplatesFolder(), "*.*", SearchOption.TopDirectoryOnly))
                {
                    File.Delete(fileName);
                }

                Program.Exit(0);
            }

            // Implement the command.

            packageCacheUri = commandLine.GetOption("--package-cache");     // This overrides the hive definition, if specified.

            if (Program.HiveLogin != null)
            {
                Console.Error.WriteLine("*** ERROR: You are logged into a hive.  You need to logout before preparing another.");
                Program.Exit(1);
            }

            if (commandLine.Arguments.Length == 0)
            {
                Console.Error.WriteLine($"*** ERROR: HIVE-DEF expected.");
                Program.Exit(1);
            }

            hiveDefPath = commandLine.Arguments[0];
            force       = commandLine.GetFlag("--force");

            HiveDefinition.ValidateFile(hiveDefPath, strict: true);

            var hiveDefinition = HiveDefinition.FromFile(hiveDefPath, strict: true);

            hiveDefinition.Provisioner = $"neon-cli:{Program.Version}";  // Identify this tool/version as the hive provisioner

            // NOTE:
            //
            // Azure has implemented a more restrictive password policy and our
            // default machine password does not meet the requirements:
            //
            // The supplied password must be between 6-72 characters long and must
            // satisfy at least 3 of password complexity requirements from the following:
            //
            //      1. Contains an uppercase character
            //      2. Contains a lowercase character
            //      3. Contains a numeric digit
            //      4. Contains a special character
            //      5. Control characters are not allowed
            //
            // It's also probably not a great idea to use a static password when
            // provisioning VMs in public clouds because it might be possible for
            // somebody to use this fact the SSH into nodes while the hive is being
            // setup and before we set the secure password at the end.
            //
            // This is less problematic for non-cloud environments because it's
            // likely that the hosts won't initially be able to receive inbound
            // Internet traffic and besides, we need to have a known password
            // embedded into the VM templates.
            //
            // We're going to handle this for cloud environments by looking
            // at [Program.MachinePassword].  If this is set to the default
            // machine password then we're going to replace it with a randomlly
            // generated password with a few extra characters to ensure that
            // it meets the target cloud's password requirements.  We'll use
            // a non-default password if the operator specified one.

            if (hiveDefinition.Hosting.IsCloudProvider && Program.MachinePassword == HiveConst.DefaulVmTemplatePassword)
            {
                Program.MachinePassword = NeonHelper.GetRandomPassword(20);

                // Append a string that guarantees that the generated password meets
                // cloud minimum requirements.

                Program.MachinePassword += ".Aa0";
            }

            // Note that hive prepare starts new log files.

            hive = new HiveProxy(hiveDefinition, Program.CreateNodeProxy <NodeDefinition>, appendLog: false, useBootstrap: true, defaultRunOptions: RunOptions.LogOutput | RunOptions.FaultOnError);

            if (File.Exists(Program.GetHiveLoginPath(HiveConst.RootUser, hive.Definition.Name)))
            {
                Console.Error.WriteLine($"*** ERROR: A hive login named [{HiveConst.RootUser}@{hive.Definition.Name}] already exists.");
                Program.Exit(1);
            }

            Program.OSProperties = OSProperties.For(hiveDefinition.HiveNode.OperatingSystem);

            // Configure global options.

            if (commandLine.HasOption("--unredacted"))
            {
                hive.SecureRunOptions = RunOptions.None;
            }

            //-----------------------------------------------------------------
            // $todo(jeff.lill):
            //
            // We're temporarily disabling redaction to make it easier to investigate
            // Vault setup issues.  Remove this line before final launch.
            //
            //      https://github.com/jefflill/NeonForge/issues/225

            hive.SecureRunOptions = RunOptions.None;

            //-----------------------------------------------------------------

            // Assign the VPN client return subnets to the manager nodes if VPN is enabled.

            if (hive.Definition.Vpn.Enabled)
            {
                var vpnSubnet            = NetworkCidr.Parse(hive.Definition.Network.VpnPoolSubnet);
                var prefixLength         = 25;
                var nextVpnSubnetAddress = vpnSubnet.Address;

                // Note that we're not going to assign the first block of addresses in the
                // VPN subnet to any managers to prevent conflicts with addresses reserved
                // by some cloud platforms at the beginning of a subnet.  Azure for example
                // reserves 4 IP addresses for DNS servers and platform provided VPNs.

                foreach (var manager in hive.Definition.SortedManagers)
                {
                    var managerVpnSubnet = new NetworkCidr(NetHelper.AddressIncrement(nextVpnSubnetAddress, VpnOptions.ServerAddressCount), prefixLength);

                    manager.VpnPoolSubnet = managerVpnSubnet.ToString();
                    nextVpnSubnetAddress  = managerVpnSubnet.NextAddress;
                }
            }

            //-----------------------------------------------------------------
            // Try to ensure that no servers are already deployed on the IP addresses defined
            // for hive nodes because provisoning over an existing hive will likely
            // corrupt the existing hive and also probably prevent the new hive from
            // provisioning correctly.
            //
            // Note that we're not going to perform this check for the [Machine] hosting
            // environment because we're expecting the bare machines to be already running
            // with the assigned addresses and we're also not going to do this for cloud
            // environments because we're assuming that the hive will run in its own private
            // network so there'll ne no possibility of conflicts.

            if (hive.Definition.Hosting.Environment != HostingEnvironments.Machine &&
                !hive.Definition.Hosting.IsCloudProvider)
            {
                Console.WriteLine();
                Console.WriteLine("Scanning for IP address conflicts...");
                Console.WriteLine();

                var pingOptions   = new PingOptions(ttl: 32, dontFragment: true);
                var pingTimeout   = TimeSpan.FromSeconds(2);
                var pingConflicts = new List <NodeDefinition>();
                var pingAttempts  = 2;

                // I'm going to use up to 20 threads at a time here for simplicity
                // rather then doing this as async operations.

                var parallelOptions = new ParallelOptions()
                {
                    MaxDegreeOfParallelism = 20
                };

                Parallel.ForEach(hive.Definition.NodeDefinitions.Values, parallelOptions,
                                 node =>
                {
                    using (var ping = new Ping())
                    {
                        // We're going to try pinging up to [pingAttempts] times for each node
                        // just in case the network it sketchy and we're losing reply packets.

                        for (int i = 0; i < pingAttempts; i++)
                        {
                            var reply = ping.Send(node.PrivateAddress, (int)pingTimeout.TotalMilliseconds);

                            if (reply.Status == IPStatus.Success)
                            {
                                lock (pingConflicts)
                                {
                                    pingConflicts.Add(node);
                                }

                                break;
                            }
                        }
                    }
                });

                if (pingConflicts.Count > 0)
                {
                    Console.Error.WriteLine($"*** ERROR: Cannot provision the hive because [{pingConflicts.Count}] other");
                    Console.Error.WriteLine($"***        machines conflict with the following hive nodes:");
                    Console.Error.WriteLine();

                    foreach (var node in pingConflicts.OrderBy(n => NetHelper.AddressToUint(IPAddress.Parse(n.PrivateAddress))))
                    {
                        Console.Error.WriteLine($"{node.PrivateAddress, 16}:    {node.Name}");
                    }

                    Program.Exit(1);
                }
            }

            //-----------------------------------------------------------------
            // Perform basic environment provisioning.  This creates basic hive components
            // such as virtual machines, networks, load balancers, public IP addresses, security
            // groups,... as required for the environment.

            hostingManager = new HostingManagerFactory(() => HostingLoader.Initialize()).GetManager(hive, Program.LogPath);

            if (hostingManager == null)
            {
                Console.Error.WriteLine($"*** ERROR: No hosting manager for the [{hive.Definition.Hosting.Environment}] hosting environment could be located.");
                Program.Exit(1);
            }

            hostingManager.HostUsername = Program.MachineUsername;
            hostingManager.HostPassword = Program.MachinePassword;
            hostingManager.ShowStatus   = !Program.Quiet;
            hostingManager.MaxParallel  = Program.MaxParallel;
            hostingManager.WaitSeconds  = Program.WaitSeconds;

            if (hostingManager.RequiresAdminPrivileges)
            {
                Program.VerifyAdminPrivileges($"Provisioning to [{hive.Definition.Hosting.Environment}] requires elevated administrator privileges.");
            }

            if (!hostingManager.Provision(force))
            {
                Program.Exit(1);
            }

            // Get the mounted drive prefix from the hosting manager.

            hive.Definition.DrivePrefix = hostingManager.DrivePrefix;

            // Ensure that the nodes have valid IP addresses.

            hive.Definition.ValidatePrivateNodeAddresses();

            var ipAddressToServer = new Dictionary <IPAddress, SshProxy <NodeDefinition> >();

            foreach (var node in hive.Nodes.OrderBy(n => n.Name))
            {
                SshProxy <NodeDefinition> duplicateServer;

                if (node.PrivateAddress == IPAddress.Any)
                {
                    throw new ArgumentException($"Node [{node.Name}] has not been assigned an IP address.");
                }

                if (ipAddressToServer.TryGetValue(node.PrivateAddress, out duplicateServer))
                {
                    throw new ArgumentException($"Nodes [{duplicateServer.Name}] and [{node.Name}] have the same IP address [{node.Metadata.PrivateAddress}].");
                }

                ipAddressToServer.Add(node.PrivateAddress, node);
            }

            //-----------------------------------------------------------------
            // Perform basic node provisioning including operating system updates & configuration,
            // and configure OpenVPN on the manager nodes so that hive setup will be
            // able to reach the nodes on all ports.

            // Write the operation begin marker to all hive node logs.

            hive.LogLine(logBeginMarker);

            var operation = $"Preparing [{hive.Definition.Name}] nodes";

            var controller =
                new SetupController <NodeDefinition>(operation, hive.Nodes)
            {
                ShowStatus  = !Program.Quiet,
                MaxParallel = Program.MaxParallel
            };

            if (!string.IsNullOrEmpty(packageCacheUri))
            {
                hive.Definition.PackageProxy = packageCacheUri;
            }

            // Prepare the nodes.

            controller.AddWaitUntilOnlineStep(timeout: TimeSpan.FromMinutes(15));
            hostingManager.AddPostProvisionSteps(controller);
            controller.AddStep("verify OS",
                               (node, stepDelay) =>
            {
                Thread.Sleep(stepDelay);
                CommonSteps.VerifyOS(node);
            });

            controller.AddStep("prepare",
                               (node, stepDelay) =>
            {
                Thread.Sleep(stepDelay);
                CommonSteps.PrepareNode(node, hive.Definition, shutdown: false);
            },
                               stepStaggerSeconds: hive.Definition.Setup.StepStaggerSeconds);

            // Add any VPN configuration steps.

            if (hive.Definition.Vpn.Enabled)
            {
                controller.AddGlobalStep("vpn credentials", () => CreateVpnCredentials());
                controller.AddStep("vpn server",
                                   (node, stepDelay) =>
                {
                    Thread.Sleep(stepDelay);
                    ConfigManagerVpn(node);
                },
                                   node => node.Metadata.IsManager);

                // Add a step to establish a VPN connection if we're provisioning to a cloud.
                // We specifically don't want to do this if we're provisioning to a on-premise
                // datacenter because we're assuming that we're already directly connected to
                // the LAN while preparing and setting up the hive.

                if (hive.Definition.Hosting.IsCloudProvider)
                {
                    controller.AddStep("vpn connect",
                                       (manager, stepDelay) =>
                    {
                        Thread.Sleep(stepDelay);

                        // Create a hive login with just enough credentials to connect the VPN.
                        // Note that this isn't really a node specific command but I wanted to
                        // be able to display the connection status somewhere.

                        var vpnLogin =
                            new HiveLogin()
                        {
                            Definition     = hive.Definition,
                            VpnCredentials = vpnCredentials
                        };

                        // Ensure that we don't have an old VPN client for the hive running.

                        HiveHelper.VpnClose(vpnLogin.Definition.Name);

                        // ...and then start a new one.

                        HiveHelper.VpnOpen(vpnLogin,
                                           onStatus: message => manager.Status = $"{message}",
                                           onError: message => manager.Status  = $"ERROR: {message}");
                    },
                                       n => n == hive.FirstManager);
                }

                // Perform any post-VPN setup provisioning required by the hosting provider.

                hostingManager.AddPostVpnSteps(controller);
            }

            if (!controller.Run())
            {
                // Write the operation end/failed marker to all hive node logs.

                hive.LogLine(logFailedMarker);

                Console.Error.WriteLine("*** ERROR: One or more configuration steps failed.");
                Program.Exit(1);
            }

            // Write the hive login file.

            var hiveLoginPath = Program.GetHiveLoginPath(HiveConst.RootUser, hive.Definition.Name);
            var hiveLogin     = new HiveLogin()
            {
                Path                 = hiveLoginPath,
                Username             = HiveConst.RootUser,
                Definition           = hive.Definition,
                SshUsername          = Program.MachineUsername,
                SshPassword          = Program.MachinePassword,
                SshProvisionPassword = Program.MachinePassword,
                SetupPending         = true
            };

            if (hive.Definition.Vpn.Enabled)
            {
                hiveLogin.VpnCredentials = vpnCredentials;
            }

            // Generate the hive certificates.

            const int bitCount  = 2048;
            const int validDays = 365000;    // About 1,000 years.

            if (hiveLogin.HiveCertificate == null)
            {
                var hostnames = new string[]
                {
                    $"{hive.Name}.nhive.io",
                    $"*.{hive.Name}.nhive.io",
                    $"*.neon-vault.{hive.Name}.nhive.io",
                    $"*.neon-registry-cache.{hive.Name}.nhive.io",
                    $"*.neon-hivemq.{hive.Name}.nhive.io"
                };

                hiveLogin.HiveCertificate = TlsCertificate.CreateSelfSigned(hostnames, bitCount, validDays,
                                                                            issuedBy: "neonHIVE",
                                                                            issuedTo: $"neonHIVE: {hiveDefinition.Name}");

                hiveLogin.HiveCertificate.FriendlyName = $"neonHIVE: {hiveLogin.Definition.Name}";
            }

            // Persist the certificates into the hive login.

            hiveLogin.Save();

            // Write the operation end marker to all hive node logs.

            hive.LogLine(logEndMarker);
        }
Example #3
0
        /// <summary>
        /// Initializes the VPN certificate authority, and creates the OpenVPN server and
        /// root client certificates.
        /// </summary>
        /// <param name="defPath">Path to the hive definition file.</param>
        /// <param name="targetFolder">The output folder.</param>
        private void InitializeCA(string defPath, string targetFolder)
        {
            DirectNotAllowed();

            var hiveDefinition = HiveDefinition.FromFile(defPath, strict: true);

            // This implements the steps described here:
            //
            //      http://www.macfreek.nl/memory/Create_a_OpenVPN_Certificate_Authority

            // Initialize
            // ----------

            Directory.CreateDirectory(caFolder);

            // Initialize the file paths.
            //
            // IMPORTANT:
            //
            // Do not change these file names because the [VpnCaFiles] class
            // depends on this naming convention.

            var indexPath     = Path.Combine(caFolder, "index.txt");
            var caSignCnfPath = Path.Combine(caFolder, "ca-sign.cnf");
            var caCnfPath     = Path.Combine(caFolder, "ca.cnf");
            var caKeyPath     = Path.Combine(caFolder, "ca.key");
            var caReqPath     = Path.Combine(caFolder, "ca.req");
            var caCrtPath     = Path.Combine(caFolder, "ca.crt");
            var dhParamPath   = Path.Combine(caFolder, "dhparam.pem");
            var serverCnfPath = Path.Combine(caFolder, "server.cnf");
            var serverKeyPath = Path.Combine(caFolder, "server.key");
            var serverReqPath = Path.Combine(caFolder, "server.req");
            var serverCrtPath = Path.Combine(caFolder, "server.crt");
            var rootCnfPath   = Path.Combine(caFolder, $"{HiveConst.RootUser}.cnf");
            var rootReqPath   = Path.Combine(caFolder, $"{HiveConst.RootUser}.req");
            var rootKeyPath   = Path.Combine(caFolder, $"{HiveConst.RootUser}.key");
            var rootCrtPath   = Path.Combine(caFolder, $"{HiveConst.RootUser}.crt");
            var taKeyPath     = Path.Combine(caFolder, "ta.key");
            var crlnumberPath = Path.Combine(caFolder, "crlnumber");
            var crlPath       = Path.Combine(caFolder, "crl.pem");

            // Create an empty certificate index file.

            File.WriteAllText(indexPath, string.Empty);

            // CA Configuration Files
            // ----------------------
            // Create configuration files. In our setup, [ca-sign.cnf] contains the configuration for signing certificates.
            // We only use it in conjunction with the [openssl ca command]. It described the folder structure within the [ca]
            // directory, the location of support files for the CA, as well as properties of the signed certificates (duration,
            // restricted usage) as well as the policy for the name ("distinguished name") of signed certificates. Finally,
            // it lists the policy for certification revocation lists. For this small-scale CA, there is no public URL to
            // download the CRL; I plan to distribute it manually.

            var caSignCnf =
                $@"# ca-sign.cnf
# This configuration file is used by the 'ca' command, to create signed certificates.
[ ca ]
default_ca              = CA_default            # The default ca section

[ CA_default ]
dir                     = {$"{this.caFolder}"}           # Where everything is kept
certs                   = $dir/                 # Where the issued certs are kept
crl_dir                 = $dir/                 # Where the issued crl are kept
new_certs_dir           = $dir/                 # default place for new certs

private_key             = $dir/ca.key           # The private key
certificate             = $dir/ca.crt           # The CA root certificate
database                = $dir/index.txt        # List of signed certificates
serial                  = $dir/serial           # The current serial number
crlnumber               = $dir/crlnumber        # the current crl number
crl                     = $dir/crl.pem          # The current CRL
RANDFILE                = $dir/.rand            # private random number file

unique_subject          = no                    # allow multiple certificates with same subject.
default_md              = sha256                # Use hash algorithm specified in the request
default_days            = 365000                # client certificates last about 1000 years
default_crl_days        = 30                    # How often clients should download the CRL

#x509_extensions        = X509_ca               # The x509 extensions for the root certificate
#x509_extensions        = X509_server           # The x509 extensions for a server certificate
x509_extensions         = X509_client           # The x509 extensions for a client certificate

# These options control what fields from the distinguished name to show before signing.
# They are required to make sure all fields are shown.
name_opt                = ca_default            # Subject Name options
cert_opt                = ca_default            # Certificate field options

copy_extensions         = copy                  # Accept requested extensions

policy                  = policy_dn

[ X509_ca ]
# X509v3 extensions for the root certificate
basicConstraints        = CA:TRUE
nsCertType              = sslCA                 # restrict the usage
keyUsage                = keyCertSign, cRLSign  # restrict the usage
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid:always,issuer:always
#subjectAltName          = email:move            # Move email address from DN to extensions
#crlDistributionPoints   = URI:http://www.example.com/example_ca.crl

[ X509_server ]
# X509v3 extensions for server certificates
basicConstraints        = CA:FALSE
nsCertType              = server                # restrict the usage
keyUsage                = digitalSignature, keyEncipherment
extendedKeyUsage        = serverAuth            # restrict the usage
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid,issuer
#subjectAltName         = email:move            # Move email address from DN to extensions
#crlDistributionPoints  = URI:http://www.example.com/example_ca.crl

[ X509_client ]
# X509v3 extensions for client certificates
basicConstraints        = CA:FALSE
nsCertType              = client                # restrict the usage
keyUsage                = digitalSignature      # restrict the usage
extendedKeyUsage        = clientAuth            # restrict the usage
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid,issuer
#subjectAltName         = email:move            # Move email address from DN to extensions
#crlDistributionPoints  = URI:http://www.example.com/example_ca.crl

[ policy_dn ]
countryName             = supplied              # required parameter, any value allowed
stateOrProvinceName     = optional
localityName            = optional
organizationName        = match                 # required, and must match root certificate
organizationalUnitName  = optional
commonName              = supplied              # required parameter, any value allowed
emailAddress            = optional              # email in DN is deprecated, use subjectAltName
";

            File.WriteAllText(caSignCnfPath, caSignCnf);

            // The x509_extensions sections are not really required by openssl or openvpn, but
            // adds extra security by telling OpenVPN that clients may connect to servers
            // only. nsCertType is required for the OpenVPN option ns-cert-type server|client;
            // keyUsage and extendedKeyUsage are required for remote-cert-tls server|client.

            // [ca.cnf] defines the distinguished name for the certificate authority. It also
            // contains the key length (2048 is recommended nowadays, over the default of 1024),
            // and if the key should be encrypted.

            var caCnf =
                $@"# ca.cnf
# This configuration file is used by the 'req' command when the root certificates is created.
[ req ]
default_bits            = 2048                  # default strength of client certificates
default_md              = sha256
encrypt_key             = no                    # ""no"" is equivalent to -nodes
prompt                  = no
string_mask             = utf8only
distinguished_name      = ca_distinguished_name     # root certificate name
req_extensions          = req_cert_extensions
#attributes             = req_attributes

[ ca_distinguished_name ]
# root certificate name
countryName             = {hiveDefinition.Vpn.CertCountryCode}
#stateOrProvinceName    = Utrecht
#localityName           = Hometown
organizationName        = {hiveDefinition.Vpn.CertOrganization}
#organizationalUnitName = My Department Name
commonName              = ca
#emailAddress           = [email protected]   # email in DN is deprecated, use subjectAltName

[ req_cert_extensions ]
nsCertType              = server
#subjectAltName         = email:[email protected]
";

            File.WriteAllText(caCnfPath, caCnf);

            // Note that in the above examples, the email address is specified in the [subjectAltName],
            // instead of in the distinguished name. This is in accordance with PKIX standards.

            // Build CA certificate
            // --------------------
            // If your CA should be valid after the year 2038, be sure to use openssl 0.9.9 or higher.
            //
            // First create a request with the correct name, and then self-sign a certificate and create
            // a serial number file.

            Program.Execute("openssl", "req", "-new",
                            "-config", caCnfPath,
                            "-keyout", caKeyPath,
                            "-out", caReqPath);

            Program.Execute("openssl", "ca", "-batch",
                            "-config", caSignCnfPath,
                            "-extensions", "X509_ca",
                            "-days", 365000,
                            "-create_serial",
                            "-selfsign",
                            "-keyfile", caKeyPath,
                            "-in", caReqPath,
                            "-out", caCrtPath);

            // Generate Prime Numbers (the Diffie Hellman parameters)
            // ------------------------------------------------------

            Program.Execute("openssl", "dhparam", "-out", dhParamPath, "2048");

            // Build server certificate
            // ------------------------

            // First, create a configuration for the server, similar to [ca.cnf]:

            var serverCnf =
                $@"# server.cnf
# This configuration file is used by the 'req' command when the server certificate is created.
[ req ]
default_bits            = 2048
default_md              = sha256
encrypt_key             = no
prompt                  = no
string_mask             = utf8only
distinguished_name      = server_distinguished_name
req_extensions          = req_cert_extensions
#attributes             = req_attributes

[ server_distinguished_name ]
countryName             = {hiveDefinition.Vpn.CertCountryCode}
#stateOrProvinceName    = 
#localityName           = 
organizationName        = {hiveDefinition.Vpn.CertOrganization}
#organizationalUnitName = My Department Name
commonName              = server
#emailAddress           = 

[ req_cert_extensions ]
nsCertType              = server
#subjectAltName         = email:[email protected]
";

            File.WriteAllText(serverCnfPath, serverCnf);

            // Create the server request and private key.

            Program.Execute("openssl", "req", "-new",
                            "-config", serverCnfPath,
                            "-keyout", serverKeyPath,
                            "-out", serverReqPath);

            // Create the server certificate.

            Program.Execute("openssl", "ca", "-batch",
                            "-config", caSignCnfPath,
                            "-extensions", "X509_server",
                            "-in", serverReqPath,
                            "-out", serverCrtPath);

            // Build the [root] client certificate.

            try
            {
                File.WriteAllText(rootCnfPath, GetClientConfig(hiveDefinition, HiveConst.RootUser, rootPrivileges: true));

                Program.Execute("openssl", "req", "-new",
                                "-config", rootCnfPath,
                                "-keyout", rootKeyPath,
                                "-out", rootReqPath);

                Program.Execute("openssl", "ca", "-batch",
                                "-config", caSignCnfPath,
                                "-out", rootCrtPath,
                                "-in", rootReqPath);
            }
            finally
            {
                if (File.Exists(rootCnfPath))
                {
                    File.Delete(rootCnfPath);
                }
            }

            // Initialize the Certificate Revocation List (CLR) number file
            // and then generate the initial (empty) CRL.

            File.WriteAllText(crlnumberPath, "00");

            Program.Execute("openssl", "ca",
                            "-config", caSignCnfPath,
                            "-gencrl",
                            "-out", crlPath);

            // As one final additional step, we're going to generate a shared
            // key that OpenVPN can use to quickly reject packets that didn't
            // come from a client with the key.  This provides a decent amount
            // of DOS protection, especially for VPNs that only use the UDP
            // transport.

            Program.Execute("openvpn", "--genkey", "--secret", taKeyPath);

            // Copy all of the CA files to the target folder.

            Directory.CreateDirectory(targetFolder);

            foreach (var file in Directory.GetFiles(caFolder, "*.*", SearchOption.TopDirectoryOnly))
            {
                File.Copy(file, Path.Combine(targetFolder, Path.GetFileName(file)));
            }
        }