Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion test/Setup.Test/ProgramTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public async Task Install_Works()
Assert.True(certFile.Exists);
var cert = new X509Certificate2(certFile.FullName);

var hundredYearsFromNow = DateTime.UtcNow.AddDays(36500);
var hundredYearsFromNow = DateTime.Now.AddDays(36500);

Assert.InRange(cert.NotAfter, hundredYearsFromNow.AddMinutes(-1), hundredYearsFromNow.AddMinutes(1));
Comment on lines -75 to 77
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When using an Ubuntu VM, this assert would fire seemingly because the cert's NotAfter date would include the timezone I set for the VM. This test may have originally been run in a Docker container with the timezone left as UTC, so comparing with UtcNow wasn't a problem. Using Now instead should still work for the container test scenario.

[xUnit.net 00:00:02.56]     Setup.Test.ProgramTests.Install_Works [FAIL]
  Failed Setup.Test.ProgramTests.Install_Works [2 s]
  Error Message:
   Assert.InRange() Failure: Value not in range
Range:  (2126-03-24T19:04:58.3800983Z - 2126-03-24T19:06:58.3800983Z)
Actual: 2126-03-24T14:05:56.0000000-05:00
  Stack Trace:
     at Setup.Test.ProgramTests.Install_Works() in /home/parallels/src/server/test/Setup.Test/ProgramTests.cs:line 77
--- End of stack trace from previous location ---


Expand Down
95 changes: 77 additions & 18 deletions util/Setup/CertBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,33 @@ public void BuildForInstall()
}
else if (_context.App.ReadQuestion("Do you want to generate a self-signed SSL certificate?"))
{
Directory.CreateDirectory($"{_context.App.RootDirectory}/ssl/self/{_context.Install.Domain}/");
var directory = Directory.CreateDirectory($"{_context.App.RootDirectory}/ssl/self/{_context.Install.Domain}/");
Helpers.WriteLine(_context, "Generating self signed SSL certificate.");
_context.Config.Ssl = true;
_context.Install.Trusted = false;
_context.Install.SelfSignedCert = true;
Helpers.Exec("openssl req -x509 -newkey rsa:4096 -sha256 -nodes -days 36500 " +
$"-keyout /bitwarden/ssl/self/{_context.Install.Domain}/private.key " +
$"-out /bitwarden/ssl/self/{_context.Install.Domain}/certificate.crt " +
$"-reqexts SAN -extensions SAN " +
$"-config <(cat /usr/lib/ssl/openssl.cnf <(printf '[SAN]\nsubjectAltName=DNS:{_context.Install.Domain}\nbasicConstraints=CA:true')) " +
$"-subj \"/C=US/ST=California/L=Santa Barbara/O=Bitwarden Inc./OU=Bitwarden/CN={_context.Install.Domain}\"");
var opensslConfig = File.ReadAllText("/usr/lib/ssl/openssl.cnf") +
$"\n[SAN]\nsubjectAltName=DNS:{_context.Install.Domain}\nbasicConstraints=CA:true";
var opensslConfigPath = Path.GetTempFileName();

File.WriteAllText(opensslConfigPath, opensslConfig);
Helpers.Exec(
"openssl",
[
"req",
"-x509",
"-newkey", "rsa:4096",
"-sha256",
"-nodes",
"-days", "36500",
"-keyout", $"{directory.FullName}/private.key",
"-out", $"{directory.FullName}/certificate.crt",
"-reqexts", "SAN",
"-extensions", "SAN",
"-config", opensslConfigPath,
"-subj", $"/C=US/ST=California/L=Santa Barbara/O=Bitwarden Inc./OU=Bitwarden/CN={_context.Install.Domain}",
]);
File.Delete(opensslConfigPath);
Comment thread
dereknance marked this conversation as resolved.
Outdated
}
}
}
Expand All @@ -58,9 +74,14 @@ public void BuildForInstall()
{
_context.Install.Trusted = true;
_context.Install.DiffieHellman = true;
Directory.CreateDirectory($"{_context.App.RootDirectory}/letsencrypt/live/{_context.Install.Domain}/");
Helpers.Exec($"openssl dhparam -out " +
$"{_context.App.RootDirectory}/letsencrypt/live/{_context.Install.Domain}/dhparam.pem 2048");
var directory = Directory.CreateDirectory($"{_context.App.RootDirectory}/letsencrypt/live/{_context.Install.Domain}/");
Helpers.Exec(
"openssl",
[
"dhparam",
"-out", $"{directory.FullName}/dhparam.pem",
"2048",
]);
}
else if (_context.Config.Ssl && !_context.Install.SelfSignedCert)
{
Expand All @@ -71,10 +92,29 @@ public void BuildForInstall()
Helpers.WriteLine(_context, "Generating key for IdentityServer.");
_context.Install.IdentityCertPassword = Helpers.SecureRandomString(32, alpha: true, numeric: true);
Directory.CreateDirectory($"{_context.App.RootDirectory}/identity/");
Helpers.Exec("openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout identity.key " +
"-out identity.crt -subj \"/CN=Bitwarden IdentityServer\" -days 36500");
Helpers.Exec($"openssl pkcs12 -export -out {_context.App.RootDirectory}/identity/identity.pfx -inkey identity.key " +
$"-in identity.crt -passout pass:{_context.Install.IdentityCertPassword}");
Helpers.Exec(
"openssl",
[
"req",
"-x509",
"-newkey", "rsa:4096",
"-sha256",
"-nodes",
"-keyout", "identity.key",
"-out", "identity.crt",
"-subj", "/CN=Bitwarden IdentityServer",
"-days", "36500",
]);
Helpers.Exec(
"openssl",
[
"pkcs12",
"-export",
"-out", $"{_context.App.RootDirectory}/identity/identity.pfx",
"-inkey", "identity.key",
"-in", "identity.crt",
"-passout", $"pass:{_context.Install.IdentityCertPassword}",
]);

Helpers.WriteLine(_context);

Expand Down Expand Up @@ -102,10 +142,29 @@ public void BuildForUpdater()
Directory.CreateDirectory($"{_context.App.RootDirectory}/key-connector/");
var keyConnectorCertPassword = Helpers.GetValueFromEnvFile(_context.App, "key-connector",
"keyConnectorSettings__certificate__filesystemPassword");
Helpers.Exec("openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout bwkc.key " +
"-out bwkc.crt -subj \"/CN=Bitwarden Key Connector\" -days 36500");
Helpers.Exec($"openssl pkcs12 -export -out {_context.App.RootDirectory}/key-connector/bwkc.pfx -inkey bwkc.key " +
$"-in bwkc.crt -passout pass:{keyConnectorCertPassword}");
Helpers.Exec(
"openssl",
[
"req",
"-x509",
"-newkey", "rsa:4096",
"-sha256",
"-nodes",
"-keyout", "bwkc.key",
"-out", "bwkc.crt",
"-subj", "/CN=Bitwarden Key Connector",
"-days", "36500",
]);
Helpers.Exec(
"openssl",
[
"pkcs12",
"-export",
"-out", $"{_context.App.RootDirectory}/key-connector/bwkc.pfx",
"-inkey", "bwkc.key",
"-in", "bwkc.crt",
"-passout", $"pass:{keyConnectorCertPassword}"
]);
}
}
}
10 changes: 5 additions & 5 deletions util/Setup/EnvironmentFileBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,27 +178,27 @@ private void Build()
{
sw.Write(template(new TemplateModel(_globalValues)));
}
Helpers.Exec($"chmod 600 {_context.App.RootDirectory}/docker/global.env");
Helpers.Exec("chmod", ["600", $"{_context.App.RootDirectory}/docker/global.env"]);

using (var sw = File.CreateText($"{_context.App.RootDirectory}/docker/mssql.env"))
{
sw.Write(template(new TemplateModel(_mssqlValues)));
}
Helpers.Exec($"chmod 600 {_context.App.RootDirectory}/docker/mssql.env");
Helpers.Exec("chmod", ["600", $"{_context.App.RootDirectory}/docker/mssql.env"]);

Helpers.WriteLine(_context, "Building docker environment override files.");
Directory.CreateDirectory($"{_context.App.RootDirectory}/env/");
using (var sw = File.CreateText($"{_context.App.RootDirectory}/env/global.override.env"))
{
sw.Write(template(new TemplateModel(_globalOverrideValues)));
}
Helpers.Exec($"chmod 600 {_context.App.RootDirectory}/env/global.override.env");
Helpers.Exec("chmod", ["600", $"{_context.App.RootDirectory}/env/global.override.env"]);

using (var sw = File.CreateText($"{_context.App.RootDirectory}/env/mssql.override.env"))
{
sw.Write(template(new TemplateModel(_mssqlOverrideValues)));
}
Helpers.Exec($"chmod 600 {_context.App.RootDirectory}/env/mssql.override.env");
Helpers.Exec("chmod", ["600", $"{_context.App.RootDirectory}/env/mssql.override.env"]);

if (_context.Config.EnableKeyConnector)
{
Expand All @@ -207,7 +207,7 @@ private void Build()
sw.Write(template(new TemplateModel(_keyConnectorOverrideValues)));
}

Helpers.Exec($"chmod 600 {_context.App.RootDirectory}/env/key-connector.override.env");
Helpers.Exec("chmod", ["600", $"{_context.App.RootDirectory}/env/key-connector.override.env"]);
}

// Empty uid env file. Only used on Linux hosts.
Expand Down
43 changes: 22 additions & 21 deletions util/Setup/Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;

Expand Down Expand Up @@ -112,35 +111,37 @@ public static string GetValueFromEnvFile(Application config, string envFile, str
return null;
}

public static string Exec(string cmd, bool returnStdout = false)
public static string Exec(string cmd, string[] arguments = null, bool returnStdout = false, bool returnStderr = false)
{
var process = new Process
var startInfo = new ProcessStartInfo(cmd, arguments ?? [])
Copy link
Copy Markdown
Contributor Author

@dereknance dereknance Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is the main reason for this PR, passing arguments as an ArgumentList instead of as a single string.

{
StartInfo = new ProcessStartInfo
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
}
RedirectStandardOutput = returnStdout,
RedirectStandardError = returnStderr,
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
};

if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
var process = new Process { StartInfo = startInfo };

var result = new StringBuilder();

process.OutputDataReceived += (_, e) =>
{
var escapedArgs = cmd.Replace("\"", "\\\"");
process.StartInfo.FileName = "/bin/sh";
process.StartInfo.Arguments = $"-c \"{escapedArgs}\"";
}
else
if (!returnStdout || e.Data == null) return;
result.Append(e.Data);
};

process.ErrorDataReceived += (_, e) =>
{
process.StartInfo.FileName = "powershell";
process.StartInfo.Arguments = cmd;
}
if (!returnStderr || e.Data == null) return;
result.Append(e.Data);
};

process.Start();
var result = returnStdout ? process.StandardOutput.ReadToEnd() : null;
process.WaitForExit();
return result;

return result.ToString();
}

public static string ReadInput(string prompt)
Expand Down
64 changes: 55 additions & 9 deletions util/Setup/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,19 +159,65 @@ private static void Update(Application application)
// a new cert and bag to replace the old Identity.pfx. This fixes an issue that came up as a result of
// moving the project to .NET 5.
_context.Install.IdentityCertPassword = Helpers.GetValueFromEnvFile(_context.App, "global", "globalSettings__identityServer__certificatePassword");
var certCountString = Helpers.Exec($"openssl pkcs12 -nokeys -info -in {application.RootDirectory}/identity/identity.pfx " +
$"-passin pass:{_context.Install.IdentityCertPassword} 2> /dev/null | grep -c \"\\-----BEGIN CERTIFICATE----\"", true);
if (int.TryParse(certCountString, out var certCount) && certCount > 1)
var certOutput = Helpers.Exec(
"openssl",
[
"pkcs12",
"-nokeys",
"-info",
"-in", $"{application.RootDirectory}/identity/identity.pfx",
"-passin", $"pass:{_context.Install.IdentityCertPassword}",
],
returnStdout: true,
returnStderr: false);

var certCount = certOutput
.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)
.Count(line => line.Contains("-----BEGIN CERTIFICATE-----"));

if (certCount > 1)
{
// Extract key from identity.pfx
Helpers.Exec($"openssl pkcs12 -in {application.RootDirectory}/identity/identity.pfx -nocerts -nodes -out identity.key " +
$"-passin pass:{_context.Install.IdentityCertPassword} > /dev/null 2>&1");
Helpers.Exec(
"openssl",
[
"pkcs12",
"-in", $"{application.RootDirectory}/identity/identity.pfx",
"-nocerts",
"-nodes",
"-out identity.key",
"-passin", $"pass:{_context.Install.IdentityCertPassword}",
],
returnStdout: false,
returnStderr: false);

// Extract certificate from identity.pfx
Helpers.Exec($"openssl pkcs12 -in {application.RootDirectory}/identity/identity.pfx -clcerts -nokeys -out identity.crt " +
$"-passin pass:{_context.Install.IdentityCertPassword} > /dev/null 2>&1");
Helpers.Exec(
"openssl",
[
"pkcs12",
"-in", $"{application.RootDirectory}/identity/identity.pfx",
"-clcerts",
"-nokeys",
"-out", "identity.crt",
"-passin", $"pass:{_context.Install.IdentityCertPassword}",
],
returnStdout: false,
returnStderr: false);

// Create new PKCS12 bag with certificate and key
Helpers.Exec($"openssl pkcs12 -export -out {application.RootDirectory}/identity/identity.pfx -inkey identity.key " +
$"-in identity.crt -passout pass:{_context.Install.IdentityCertPassword} > /dev/null 2>&1");
Helpers.Exec(
"openssl",
[
"pkcs12",
"-export",
"-out", $"{application.RootDirectory}/identity/identity.pfx",
"-inkey", "identity.key",
"-in", $"identity.crt",
"-passout", $"pass:{_context.Install.IdentityCertPassword}"
],
returnStdout: false,
returnStderr: false);
}

if (_context.Parameters.ContainsKey("db"))
Expand Down
Loading