/// <summary> /// Creates a new hive login. /// </summary> /// <param name="commandLine">The command line.</param> private void UserCreate(CommandLine commandLine) { DirectNotAllowed(); var username = commandLine.Arguments.FirstOrDefault(); if (string.IsNullOrEmpty(username)) { Console.Error.WriteLine("***ERROR: USER argument is required."); Program.Exit(1); } var isUserValid = true; foreach (var ch in username) { if (!(char.IsLetterOrDigit(ch) || ch == '.' || ch == '_' || ch == '-')) { isUserValid = false; break; } } if (!isUserValid) { Console.WriteLine($"***ERROR: USER [{username}] is not valid. Only letters, digits, periods, underscores, or dashes are allowed."); Program.Exit(1); } switch (username.ToLowerInvariant()) { case "ca": case "dhparam": case "server": Console.WriteLine($"***ERROR: USER [{username}] is reserved by neonHIVE. Please choose another name."); Program.Exit(1); break; } var daysOption = commandLine.GetOption("--days", "365"); int days = 365; if (string.IsNullOrEmpty(daysOption) || !int.TryParse(daysOption, out days) || days <= 0) { Console.WriteLine($"***ERROR: [--days={daysOption}] is not valid. This must be a positive integer."); Program.Exit(1); } var rootPrivileges = commandLine.HasOption("--root"); RootLogin(); Directory.CreateDirectory(caFolder); try { // Retrieve the VPN certificate authority ZIP archive from Vault and extract // its contents to a temporary folder. var caZipBytes = hive.Vault.Client.ReadBytesAsync("neon-secret/vpn/ca.zip.encrypted").Result; var vpnCaFiles = VpnCaFiles.LoadZip(caZipBytes, hiveLogin.VpnCredentials.CaZipKey); vpnCaFiles.Extract(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 userCnfPath = Path.Combine(caFolder, $"{username}.cnf"); var userReqPath = Path.Combine(caFolder, $"{username}.req"); var userKeyPath = Path.Combine(caFolder, $"{username}.key"); var userCrtPath = Path.Combine(caFolder, $"{username}.crt"); var taKeyPath = Path.Combine(caFolder, "ta.key"); var crlnumberPath = Path.Combine(caFolder, "crlnumber"); var crlPath = Path.Combine(caFolder, "crl.pem"); // Build the new user client login. File.WriteAllText(userCnfPath, GetClientConfig(hive.Definition, username, rootPrivileges)); Program.Execute("openssl", "req", "-new", "-config", userCnfPath, "-keyout", userKeyPath, "-out", userReqPath); Program.Execute("openssl", "ca", "-batch", "-config", caSignCnfPath, "-days", days, "-out", userCrtPath, "-in", userReqPath); // Generate the new hive login file and also write its name // to [new-login.txt] so the outer shim will know what it is. var newLogin = hiveLogin.Clone(); newLogin.Username = username; newLogin.VpnCredentials.UserCert = VpnCaFiles.NormalizePem(File.ReadAllText(userCrtPath)); newLogin.VpnCredentials.UserKey = File.ReadAllText(userKeyPath); if (!rootPrivileges) { newLogin.ClearRootSecrets(); } File.WriteAllText($"{username}@{newLogin.HiveName}.login.json", NeonHelper.JsonSerialize(newLogin, Formatting.Indented)); File.WriteAllText("new-login.txt", $"{username}@{newLogin.HiveName}.login.json"); // ZIP the CA files and store them to the hive Vault. vpnCaFiles = VpnCaFiles.LoadFolder(caFolder); vpnCaFiles.Clean(); hive.Vault.Client.WriteBytesAsync("neon-secret/vpn/ca.zip.encrypted", vpnCaFiles.ToZipBytes()).Wait(); } finally { Directory.Delete(caFolder, recursive: true); HiveHelper.CloseHive(); } }
/// <summary> /// Initializes the VPN certificate authority as well as the server and root user's certificates and keys. /// </summary> private void CreateVpnCredentials() { // This is a bit tricky: We're going to invoke the [neon vpn ca ...] command to // initialize the hive's certificate authority files. This command must be // run in the [neon-cli] container so we need to detect whether we're already // running in the tool container and do the right thing. // // Note that the we can't pass the original hive definition file to the command // because the command will shim into a [neon-cli] container and any environment // variable references within the definition won't be able to be resolved because // the environment variables aren't mapped into the container. // // The solution is to persist a temporary copy of the loaded hive definition // that has already resolved environment variables to the neonFORGE temp folder // and pass that. The user's neonFORGE folder is encrypted in place so doing this // will be as safe as storing hive logins there. string tempCaFolder; string tempDefPath; if (HiveHelper.InToolContainer) { tempCaFolder = "/shim/ca"; } else { tempCaFolder = Path.Combine(Program.HiveTempFolder, Guid.NewGuid().ToString("D")); } tempDefPath = Path.Combine(HiveHelper.GetTempFolder(), $"{Guid.NewGuid().ToString("D").ToLowerInvariant()}.def.json"); File.WriteAllText(tempDefPath, NeonHelper.JsonSerialize(hive.Definition, Formatting.Indented)); try { Directory.CreateDirectory(tempCaFolder); // Execute the [neon vpn cert init ...] command to generate the certificate // authority files and the root client certificate and key and then load // the files into a [VpnCaFiles] object. var neonExecutable = NeonHelper.IsWindows ? "neon.cmd" : "neon"; if (HiveHelper.InToolContainer) { Program.Execute(neonExecutable, "vpn", "ca", tempDefPath, tempCaFolder); } else { Program.Execute(neonExecutable, "vpn", "ca", tempDefPath, tempCaFolder); } vpnCaFiles = VpnCaFiles.LoadFolder(tempCaFolder); } finally { if (Directory.Exists(tempCaFolder)) { Directory.Delete(tempCaFolder, recursive: true); } if (File.Exists(tempDefPath)) { File.Delete(tempDefPath); } } }
/// <summary> /// Revokes a user certificate. /// </summary> /// <param name="commandLine">The command line.</param> private void UserRevoke(CommandLine commandLine) { DirectNotAllowed(); var restartVpn = commandLine.HasOption("--restart-vpn"); var thumbprint = commandLine.Arguments.FirstOrDefault(); if (string.IsNullOrEmpty(thumbprint)) { Console.Error.WriteLine("*** ERROR: THUMPRINT expected."); Program.Exit(1); } thumbprint = thumbprint.ToLowerInvariant(); RootLogin(); try { var vpnCaFiles = GetVpnCaFiles(); var certInfo = ListCerts(vpnCaFiles).Where(c => c.Thumbprint.ToLowerInvariant() == thumbprint).FirstOrDefault(); if (certInfo == null) { Console.Error.WriteLine($"*** ERROR: Certificate with thumbprint [{thumbprint}] is not known."); Program.Exit(1); } if (!certInfo.IsValid) { Console.Error.WriteLine($"*** ERROR: Certificate with thumbprint [{thumbprint}] is already revoked."); Program.Exit(1); } // Initialize the file paths. // // IMPORTANT: // // Do not change these file names because the [VpnCaFiles] class // depends on this naming convention. Directory.CreateDirectory(caFolder); vpnCaFiles.Extract(caFolder); 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 taKeyPath = Path.Combine(caFolder, "ta.key"); var crlnumberPath = Path.Combine(caFolder, "crlnumber"); var crlPath = Path.Combine(caFolder, "crl.pem"); // Mark the certificate as revoked. Program.Execute("openssl", "ca", "-config", caSignCnfPath, "-crl_reason", "unspecified", "-revoke", $"{Path.Combine(caFolder, thumbprint.ToUpperInvariant())}.pem", "-cert", caCrtPath, "-keyfile", caKeyPath); // Generate the new CRL file. Program.Execute("openssl", "ca", "-config", caSignCnfPath, "-gencrl", "-out", crlPath); // Save the CA files back to the hive Vault. vpnCaFiles = VpnCaFiles.LoadFolder(caFolder); hive.Vault.Client.WriteBytesAsync("neon-secret/vpn/ca.zip.encrypted", vpnCaFiles.ToZipBytes()).Wait(); // Write the updated CRL to each manager. var crlText = vpnCaFiles.GetFile("crl.pem"); Console.WriteLine(); foreach (var manager in hive.Managers) { Console.WriteLine($"*** {manager.Name}: Revoking"); manager.UploadText("/etc/openvpn/crl.pem", crlText); manager.SudoCommand("chmod 664 /etc/openvpn/crl.pem"); } // Restart OpenVPN on each manager if requested. if (restartVpn) { Console.WriteLine(); foreach (var manager in hive.Managers) { Console.WriteLine($"*** {manager.Name}: Restarting OpenVPN"); manager.SudoCommand("systemctl restart openvpn"); Thread.Sleep(TimeSpan.FromSeconds(5)); } } } finally { HiveHelper.CloseHive(); } }