You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
382 lines
14 KiB
382 lines
14 KiB
using System; |
|
using System.Collections.Generic; |
|
using System.Diagnostics; |
|
using System.IO; |
|
using System.Runtime.InteropServices; |
|
|
|
namespace Setup |
|
{ |
|
public class Program |
|
{ |
|
private static string[] _args = null; |
|
private static IDictionary<string, string> _parameters = null; |
|
private static string _outputDir = null; |
|
private static string _domain = null; |
|
private static string _url = null; |
|
private static string _identityCertPassword = null; |
|
private static bool _ssl = false; |
|
private static bool _selfSignedSsl = false; |
|
private static bool _letsEncrypt = false; |
|
private static string _installationId = null; |
|
private static string _installationKey = null; |
|
private static bool _push = false; |
|
|
|
public static void Main(string[] args) |
|
{ |
|
_args = args; |
|
_parameters = ParseParameters(); |
|
|
|
_installationId = _parameters.ContainsKey("install_id") ? |
|
_parameters["install_id"].ToLowerInvariant() : null; |
|
_installationKey = _parameters.ContainsKey("install_key") ? |
|
_parameters["install_key"].ToLowerInvariant() : null; |
|
_outputDir = _parameters.ContainsKey("out") ? |
|
_parameters["out"].ToLowerInvariant() : "/etc/bitwarden"; |
|
_domain = _parameters.ContainsKey("domain") ? |
|
_parameters["domain"].ToLowerInvariant() : "localhost"; |
|
_letsEncrypt = _parameters.ContainsKey("letsencrypt") ? |
|
_parameters["letsencrypt"].ToLowerInvariant() == "y" : false; |
|
_ssl = _letsEncrypt || (_parameters.ContainsKey("ssl") ? |
|
_parameters["ssl"].ToLowerInvariant() == "y" : false); |
|
|
|
_ssl = _letsEncrypt; |
|
if(!_letsEncrypt) |
|
{ |
|
Console.Write("(!) Are you using your own SSL certificate? (y/n): "); |
|
_ssl = Console.ReadLine().ToLowerInvariant() == "y"; |
|
|
|
if(_ssl) |
|
{ |
|
Console.WriteLine("Make sure 'certificate.crt' and 'private.key' are provided in the " + |
|
"appropriate directory (see setup instructions)."); |
|
} |
|
} |
|
|
|
_identityCertPassword = Helpers.SecureRandomString(32, alpha: true, numeric: true); |
|
MakeCerts(); |
|
|
|
_url = _ssl ? $"https://{_domain}" : $"http://{_domain}"; |
|
BuildNginxConfig(); |
|
|
|
Console.Write("(!) Do you want to use push notifications? (y/n): "); |
|
_push = Console.ReadLine().ToLowerInvariant() == "y"; |
|
|
|
BuildEnvironmentFiles(); |
|
BuildAppSettingsFiles(); |
|
} |
|
|
|
private static void MakeCerts() |
|
{ |
|
if(!_ssl) |
|
{ |
|
Console.Write("(!) Do you want to generate a self signed SSL certificate? (y/n): "); |
|
if(Console.ReadLine().ToLowerInvariant() == "y") |
|
{ |
|
Directory.CreateDirectory($"/bitwarden/ssl/self/{_domain}/"); |
|
Console.WriteLine("Generating self signed SSL certificate."); |
|
_ssl = _selfSignedSsl = true; |
|
Exec("openssl req -x509 -newkey rsa:4096 -sha256 -nodes -days 365 " + |
|
$"-keyout /bitwarden/ssl/self/{_domain}/private.key " + |
|
$"-out /bitwarden/ssl/self/{_domain}/certificate.crt " + |
|
$"-subj \"/C=US/ST=New York/L=New York/O=8bit Solutions LLC/OU=bitwarden/CN={_domain}\""); |
|
} |
|
} |
|
|
|
if(_letsEncrypt) |
|
{ |
|
Directory.CreateDirectory($"/bitwarden/letsencrypt/live/{_domain}/"); |
|
Exec($"openssl dhparam -out /bitwarden/letsencrypt/live/{_domain}/dhparam.pem 2048"); |
|
} |
|
|
|
Console.WriteLine("Generating key for IdentityServer."); |
|
Directory.CreateDirectory("/bitwarden/identity/"); |
|
Exec("openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout identity.key " + |
|
"-out identity.crt -subj \"/CN=bitwarden IdentityServer\" -days 10950"); |
|
Exec("openssl pkcs12 -export -out /bitwarden/identity/identity.pfx -inkey identity.key " + |
|
$"-in identity.crt -certfile identity.crt -passout pass:{_identityCertPassword}"); |
|
} |
|
|
|
private static void BuildNginxConfig() |
|
{ |
|
Directory.CreateDirectory("/bitwarden/nginx/"); |
|
var sslCiphers = "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:" + |
|
"DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:" + |
|
"ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:" + |
|
"ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:" + |
|
"AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4:@STRENGTH"; |
|
|
|
var dh = _letsEncrypt; |
|
if(_ssl && !_selfSignedSsl && !_letsEncrypt) |
|
{ |
|
Console.Write("(!) Use Diffie Hellman ephemeral parameters for SSL (requires dhparam.pem)? (y/n): "); |
|
dh = Console.ReadLine().ToLowerInvariant() == "y"; |
|
} |
|
|
|
var trusted = _letsEncrypt; |
|
if(_ssl && !_selfSignedSsl && !_letsEncrypt) |
|
{ |
|
Console.Write("(!) Is this a trusted SSL certificate (requires ca.crt)? (y/n): "); |
|
trusted = Console.ReadLine().ToLowerInvariant() == "y"; |
|
} |
|
|
|
var sslPath = _letsEncrypt ? $"/etc/letsencrypt/live/{_domain}" : |
|
_selfSignedSsl ? $"/etc/ssl/self/{_domain}" : $"/etc/ssl/{_domain}"; |
|
var certFile = _letsEncrypt ? "fullchain.pem" : "certificate.crt"; |
|
var keyFile = _letsEncrypt ? "privkey.pem" : "private.key"; |
|
var caFile = _letsEncrypt ? "fullchain.pem" : "ca.crt"; |
|
|
|
Console.WriteLine("Building nginx config."); |
|
using(var sw = File.CreateText("/bitwarden/nginx/default.conf")) |
|
{ |
|
sw.WriteLine($@"server {{ |
|
listen 80 default_server; |
|
listen [::]:80 default_server; |
|
server_name {_domain};"); |
|
|
|
if(_ssl) |
|
{ |
|
sw.WriteLine($@" return 301 https://$server_name$request_uri; |
|
}} |
|
|
|
server {{ |
|
listen 443 ssl http2; |
|
listen [::]:443 ssl http2; |
|
server_name {_domain}; |
|
|
|
ssl_certificate {sslPath}/{certFile}; |
|
ssl_certificate_key {sslPath}/{keyFile}; |
|
|
|
ssl_session_timeout 30m; |
|
ssl_session_cache shared:SSL:20m; |
|
ssl_session_tickets off;"); |
|
|
|
if(dh) |
|
{ |
|
sw.WriteLine($@" |
|
# Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits |
|
ssl_dhparam {sslPath}/dhparam.pem;"); |
|
} |
|
|
|
sw.WriteLine($@" |
|
# SSL protocols TLS v1~TLSv1.2 are allowed. Disabed SSLv3 |
|
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; |
|
# Disabled insecure ciphers suite. For example, MD5, DES, RC4, PSK |
|
ssl_ciphers ""{sslCiphers}""; |
|
# enables server-side protection from BEAST attacks |
|
ssl_prefer_server_ciphers on;"); |
|
|
|
if(trusted) |
|
{ |
|
sw.WriteLine($@" |
|
# OCSP Stapling --- |
|
# fetch OCSP records from URL in ssl_certificate and cache them |
|
ssl_stapling on; |
|
ssl_stapling_verify on; |
|
|
|
## verify chain of trust of OCSP response using Root CA and Intermediate certs |
|
ssl_trusted_certificate {sslPath}/{caFile}; |
|
|
|
resolver 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=300s;"); |
|
} |
|
|
|
sw.WriteLine($@" |
|
# This will enforce HTTP browsing into HTTPS and avoid ssl stripping attack. 6 months age |
|
add_header Strict-Transport-Security max-age=15768000;"); |
|
} |
|
|
|
sw.WriteLine($@" |
|
# X-Frame-Options is to prevent from clickJacking attack |
|
add_header X-Frame-Options SAMEORIGIN; |
|
|
|
# disable content-type sniffing on some browsers. |
|
add_header X-Content-Type-Options nosniff; |
|
|
|
# This header enables the Cross-site scripting (XSS) filter |
|
add_header X-XSS-Protection ""1; mode=block""; |
|
|
|
# This header controls what referrer information is shared |
|
add_header Referrer-Policy same-origin; |
|
|
|
# Content-Security-Policy is set via meta tag on the website so it is not included here"); |
|
|
|
sw.WriteLine($@" |
|
location / {{ |
|
proxy_pass http://web/; |
|
proxy_set_header X-Real-IP $remote_addr; |
|
proxy_set_header Host $host; |
|
proxy_set_header X-Forwarded-Proto $scheme; |
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
|
proxy_set_header X-Url-Scheme $scheme; |
|
proxy_redirect off; |
|
}} |
|
|
|
location = /app-id.json {{ |
|
proxy_pass http://web/app-id.json; |
|
proxy_hide_header Content-Type; |
|
add_header Content-Type $fido_content_type; |
|
proxy_redirect off; |
|
}} |
|
|
|
location /attachments/ {{ |
|
proxy_pass http://attachments/; |
|
proxy_set_header X-Real-IP $remote_addr; |
|
proxy_set_header Host $host; |
|
proxy_set_header X-Forwarded-Proto $scheme; |
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
|
proxy_set_header X-Url-Scheme $scheme; |
|
proxy_redirect off; |
|
}} |
|
|
|
location /api/ {{ |
|
proxy_pass http://api/; |
|
proxy_set_header X-Real-IP $remote_addr; |
|
proxy_set_header Host $host; |
|
proxy_set_header X-Forwarded-Proto $scheme; |
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
|
proxy_set_header X-Url-Scheme $scheme; |
|
proxy_redirect off; |
|
}} |
|
|
|
location /identity/ {{ |
|
proxy_pass http://identity/; |
|
proxy_set_header X-Real-IP $remote_addr; |
|
proxy_set_header Host $host; |
|
proxy_set_header X-Forwarded-Proto $scheme; |
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
|
proxy_set_header X-Url-Scheme $scheme; |
|
proxy_redirect off; |
|
}} |
|
}}"); |
|
} |
|
} |
|
|
|
private static void BuildEnvironmentFiles() |
|
{ |
|
Console.WriteLine("Building docker environment override files."); |
|
Directory.CreateDirectory("/bitwarden/docker/"); |
|
var dbPass = _parameters.ContainsKey("db_pass") ? _parameters["db_pass"].ToLowerInvariant() : "REPLACE"; |
|
var dbConnectionString = "Server=tcp:mssql,1433;Initial Catalog=vault;Persist Security Info=False;User ID=sa;" + |
|
$"Password={dbPass};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=True;" + |
|
"Connection Timeout=30;"; |
|
|
|
using(var sw = File.CreateText("/bitwarden/docker/global.override.env")) |
|
{ |
|
sw.Write($@"globalSettings:baseServiceUri:vault={_url} |
|
globalSettings:baseServiceUri:api={_url}/api |
|
globalSettings:baseServiceUri:identity={_url}/identity |
|
globalSettings:sqlServer:connectionString={dbConnectionString} |
|
globalSettings:identityServer:certificatePassword={_identityCertPassword} |
|
globalSettings:attachment:baseDirectory={_outputDir}/core/attachments |
|
globalSettings:attachment:baseUrl={_url}/attachments |
|
globalSettings:dataProtection:directory={_outputDir}/core/aspnet-dataprotection |
|
globalSettings:logDirectory={_outputDir}/core/logs |
|
globalSettings:licenseDirectory={_outputDir}/core/licenses |
|
globalSettings:duo:aKey={Helpers.SecureRandomString(64, alpha: true, numeric: true)} |
|
globalSettings:installation:id={_installationId} |
|
globalSettings:installation:key={_installationKey} |
|
globalSettings:yubico:clientId=REPLACE |
|
globalSettings:yubico:key=REPLACE"); |
|
|
|
if(!_push) |
|
{ |
|
sw.Write(@" |
|
globalSettings:pushRelayBaseUri=REPLACE"); |
|
} |
|
} |
|
|
|
using(var sw = File.CreateText("/bitwarden/docker/mssql.override.env")) |
|
{ |
|
sw.Write($@"ACCEPT_EULA=Y |
|
MSSQL_PID=Express |
|
SA_PASSWORD={dbPass}"); |
|
} |
|
} |
|
|
|
private static void BuildAppSettingsFiles() |
|
{ |
|
Console.WriteLine("Building app settings."); |
|
Directory.CreateDirectory("/bitwarden/web/"); |
|
using(var sw = File.CreateText("/bitwarden/web/settings.js")) |
|
{ |
|
sw.Write($@"var bitwardenAppSettings = {{ |
|
apiUri: ""{_url}/api"", |
|
identityUri: ""{_url}/identity"", |
|
stripeKey: null, |
|
braintreeKey: null, |
|
whitelistDomains: [""{_domain}""] |
|
}};"); |
|
} |
|
} |
|
|
|
private static void BuildAppId() |
|
{ |
|
Console.WriteLine("Building FIDO U2F app id."); |
|
Directory.CreateDirectory("/bitwarden/web/"); |
|
using(var sw = File.CreateText("/bitwarden/web/app-id.json")) |
|
{ |
|
sw.Write($@"{{ |
|
""trustedFacets"": [ |
|
{{ |
|
""version"": {{ |
|
""major"": 1, |
|
""minor"": 0 |
|
}}, |
|
""ids"": [ |
|
""{_url}"", |
|
""ios:bundle-id:com.8bit.bitwarden"", |
|
""android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI"" |
|
] |
|
}} |
|
] |
|
}}"); |
|
} |
|
} |
|
|
|
private static IDictionary<string, string> ParseParameters() |
|
{ |
|
var dict = new Dictionary<string, string>(); |
|
for(var i = 0; i < _args.Length; i = i + 2) |
|
{ |
|
if(!_args[i].StartsWith("-")) |
|
{ |
|
continue; |
|
} |
|
|
|
dict.Add(_args[i].Substring(1), _args[i + 1]); |
|
} |
|
|
|
return dict; |
|
} |
|
|
|
private static string Exec(string cmd) |
|
{ |
|
var process = new Process |
|
{ |
|
StartInfo = new ProcessStartInfo |
|
{ |
|
RedirectStandardOutput = true, |
|
UseShellExecute = false, |
|
CreateNoWindow = true, |
|
WindowStyle = ProcessWindowStyle.Hidden |
|
} |
|
}; |
|
|
|
if(!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
|
{ |
|
var escapedArgs = cmd.Replace("\"", "\\\""); |
|
process.StartInfo.FileName = "/bin/bash"; |
|
process.StartInfo.Arguments = $"-c \"{escapedArgs}\""; |
|
} |
|
else |
|
{ |
|
process.StartInfo.FileName = "powershell"; |
|
process.StartInfo.Arguments = cmd; |
|
} |
|
|
|
process.Start(); |
|
string result = process.StandardOutput.ReadToEnd(); |
|
process.WaitForExit(); |
|
return result; |
|
} |
|
} |
|
}
|
|
|