diff --git a/bitwarden-core.sln b/bitwarden-core.sln index 418a36727..e2608287c 100644 --- a/bitwarden-core.sln +++ b/bitwarden-core.sln @@ -28,7 +28,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Billing", "src\Billing\Bill EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Identity", "src\Identity\Identity.csproj", "{04148736-3C0B-445E-8B74-2020E7A53502}" EndProject -Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "Docker", "docker\Docker.dcproj", "{026DDB58-F0DB-4089-8168-83015AF785AE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Setup", "util\Setup\Setup.csproj", "{EF2164EF-1FC0-4518-A2ED-CE02D3630B00}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -62,10 +62,10 @@ Global {04148736-3C0B-445E-8B74-2020E7A53502}.Debug|Any CPU.Build.0 = Debug|Any CPU {04148736-3C0B-445E-8B74-2020E7A53502}.Release|Any CPU.ActiveCfg = Release|Any CPU {04148736-3C0B-445E-8B74-2020E7A53502}.Release|Any CPU.Build.0 = Release|Any CPU - {026DDB58-F0DB-4089-8168-83015AF785AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {026DDB58-F0DB-4089-8168-83015AF785AE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {026DDB58-F0DB-4089-8168-83015AF785AE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {026DDB58-F0DB-4089-8168-83015AF785AE}.Release|Any CPU.Build.0 = Release|Any CPU + {EF2164EF-1FC0-4518-A2ED-CE02D3630B00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF2164EF-1FC0-4518-A2ED-CE02D3630B00}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF2164EF-1FC0-4518-A2ED-CE02D3630B00}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF2164EF-1FC0-4518-A2ED-CE02D3630B00}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -77,6 +77,7 @@ Global {B78A6C74-1A24-48C6-802A-13BE3E4DAFF1} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} {02BC2982-ED8D-4A6D-A41E-092B3DAEB98A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D} {04148736-3C0B-445E-8B74-2020E7A53502} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D} + {EF2164EF-1FC0-4518-A2ED-CE02D3630B00} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F} diff --git a/build.ps1 b/build.ps1 index 74b93fdd2..fa14d01cd 100644 --- a/build.ps1 +++ b/build.ps1 @@ -7,3 +7,4 @@ echo "==================" & $dir\src\Api\build.ps1 & $dir\src\Identity\build.ps1 & $dir\nginx\build.ps1 +& $dir\util\Setup\build.ps1 diff --git a/build.sh b/build.sh index ca0a3c02b..168e955b8 100644 --- a/build.sh +++ b/build.sh @@ -9,3 +9,4 @@ echo -e "==================" $DIR/src/Api/build.sh $DIR/src/Identity/build.sh $DIR/nginx/build.sh +$DIR/util/Setup/build.sh diff --git a/docker/Docker.dcproj b/docker/Docker.dcproj deleted file mode 100644 index 53500d856..000000000 --- a/docker/Docker.dcproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - 2.0 - Linux - 026ddb58-f0db-4089-8168-83015af785ae - True - http://localhost:{ServicePort} - api - - - - - \ No newline at end of file diff --git a/docker/docker-compose.linux.yml b/docker/docker-compose.linux.yml index 3bf7a1ab5..844e51ae3 100644 --- a/docker/docker-compose.linux.yml +++ b/docker/docker-compose.linux.yml @@ -4,12 +4,11 @@ services: mssql: volumes: - /etc/bitwarden/mssql_data:/var/opt/mssql/data - api: - volumes: - - /etc/bitwarden/core:/etc/core identity: volumes: - - /etc/bitwarden/core:/etc/core + - /etc/bitwarden/identity:/etc/bitwarden/identity nginx: volumes: + - /etc/bitwarden/nginx:/etc/bitwarden/nginx - /etc/bitwarden/letsencrypt:/etc/letsencrypt + - /etc/bitwarden/ssl:/etc/certificates diff --git a/docker/docker-compose.override.yml b/docker/docker-compose.override.yml index 4e08d1063..3f98220d5 100644 --- a/docker/docker-compose.override.yml +++ b/docker/docker-compose.override.yml @@ -4,8 +4,13 @@ services: mssql: volumes: - mssql_data:/var/opt/mssql/data + identity: + volumes: + - c:/bitwarden/identity:/etc/bitwarden/identity nginx: volumes: + - c:/bitwarden/nginx:/etc/bitwarden/nginx - c:/bitwarden/letsencrypt:/etc/letsencrypt + - c:/bitwarden/ssl:/etc/certificates volumes: mssql_data: diff --git a/docker/docker-compose.windows.yml b/docker/docker-compose.windows.yml index e6c3e498f..3f98220d5 100644 --- a/docker/docker-compose.windows.yml +++ b/docker/docker-compose.windows.yml @@ -4,14 +4,13 @@ services: mssql: volumes: - mssql_data:/var/opt/mssql/data - api: - volumes: - - c:/bitwarden/core:/etc/core identity: volumes: - - c:/bitwarden/core:/etc/core + - c:/bitwarden/identity:/etc/bitwarden/identity nginx: volumes: + - c:/bitwarden/nginx:/etc/bitwarden/nginx - c:/bitwarden/letsencrypt:/etc/letsencrypt + - c:/bitwarden/ssl:/etc/certificates volumes: mssql_data: diff --git a/nginx/Dockerfile b/nginx/Dockerfile index e86c662f4..ff3e12764 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -3,7 +3,6 @@ FROM nginx:stable RUN rm /etc/nginx/nginx.conf COPY nginx.conf /etc/nginx/nginx.conf -RUN rm /etc/nginx/conf.d/default.conf -COPY default.conf /etc/nginx/conf.d/default.conf - -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file +COPY entrypoint.sh / +RUN chmod +x /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] diff --git a/nginx/default.conf b/nginx/default.conf deleted file mode 100644 index d655e1d16..000000000 --- a/nginx/default.conf +++ /dev/null @@ -1,73 +0,0 @@ -server { - listen 80 default_server; - listen [::]:80 default_server; - server_name bw.kylespearrin.com; - return 301 https://$server_name$request_uri; -} - -server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name bw.kylespearrin.com; - - ssl_certificate /etc/letsencrypt/live/bw.kylespearrin.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/bw.kylespearrin.com/privkey.pem; - - ssl_session_timeout 30m; - ssl_session_cache shared:SSL:20m; - ssl_session_tickets off; - - # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits - ssl_dhparam /etc/letsencrypt/live/bw.kylespearrin.com/dhparam.pem; - - # 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 "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"; - # enables server-side protection from BEAST attacks - ssl_prefer_server_ciphers on; - - # 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 /etc/letsencrypt/live/bw.kylespearrin.com/fullchain.pem; - - resolver 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=300s; - - # Headers - - # 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 will enforce HTTP browsing into HTTPS and avoid ssl stripping attack - #add_header Strict-Transport-Security max-age=15768000; - - 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; - } -} diff --git a/nginx/entrypoint.sh b/nginx/entrypoint.sh new file mode 100644 index 000000000..05f7b1079 --- /dev/null +++ b/nginx/entrypoint.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +cp /etc/bitwarden/nginx/default.conf /etc/nginx/conf.d/default.conf +nginx -g 'daemon off;' diff --git a/scripts/run.ps1 b/scripts/run.ps1 index 577f06f21..81c521040 100644 --- a/scripts/run.ps1 +++ b/scripts/run.ps1 @@ -1,7 +1,8 @@ -$dockerDir="../docker" +$dir = Split-Path -Parent $MyInvocation.MyCommand.Path +$dockerDir="${dir}\..\docker" docker --version docker-compose --version -docker-compose -f $dockerDir/docker-compose.yml -f $dockerDir/docker-compose.windows.yml down -docker-compose -f $dockerDir/docker-compose.yml -f $dockerDir/docker-compose.windows.yml up -d +docker-compose -f ${dockerDir}\docker-compose.yml -f ${dockerDir}\docker-compose.windows.yml down +docker-compose -f ${dockerDir}\docker-compose.yml -f ${dockerDir}\docker-compose.windows.yml up -d diff --git a/scripts/setup.ps1 b/scripts/setup.ps1 index 1f6413de6..a637f934e 100644 --- a/scripts/setup.ps1 +++ b/scripts/setup.ps1 @@ -1,13 +1,12 @@ param ( [string]$outputDir = "c:/bitwarden", [string]$domain = $( Read-Host "Please enter your domain name (i.e. bitwarden.company.com)" ), - [string]$email = $( Read-Host "Please enter your email address (used to generate an HTTPS certificate with LetsEncrypt)" ) + [string]$email = $( Read-Host "Please enter your email address: " ), + [string]$letsencrypt = $( Read-Host "Generate Let's Encrypt Cert (y/n)" ) ) $dockerDir="../docker" -$certPassword=-join ((48..57) + (97..122) | Get-Random -Count 32 | % {[char]$_}) $databasePassword=-join ((48..57) + (97..122) | Get-Random -Count 32 | % {[char]$_}) -$duoKey=-join ((48..57) + (97..122) | Get-Random -Count 32 | % {[char]$_}) docker --version @@ -15,23 +14,6 @@ docker --version #docker run -it --rm -p 80:80 -v $outputDir/letsencrypt:/etc/letsencrypt/ certbot/certbot certonly --standalone --noninteractive --preferred-challenges http --email $email --agree-tos -d $domain #docker run -it --rm -v $outputDir/letsencrypt/live:/certificates/ bitwarden/openssl openssl dhparam -out /certificates/$domain/dhparam.pem 2048 -mkdir -p $outputDir/core -docker run -it --rm -v $outputDir/core:/certificates bitwarden/openssl openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout /certificates/identity.key -out /certificates/identity.crt -subj "/CN=bitwarden IdentityServer" -days 10950 -docker run -it --rm -v $outputDir/core:/certificates bitwarden/openssl openssl pkcs12 -export -out /certificates/identity.pfx -inkey /certificates/identity.key -in /certificates/identity.crt -certfile /certificates/identity.crt -passout pass:$certPassword -rm $outputDir/core/identity.key -rm $outputDir/core/identity.crt +docker run -it --rm -v ${outputDir}:/bitwarden bitwarden/setup dotnet Setup.dll -domain ${domain} -letsencrypt ${letsencrypt} -db_pass ${databasePassword} -Add-Content $dockerDir/global.override.env " -globalSettings:baseServiceUri:vault=https://$domain -globalSettings:baseServiceUri:api=https://$domain/api -globalSettings:baseServiceUri:identity=https://$domain/identity -globalSettings:sqlServer:connectionString=Server=tcp:mssql,1433;Initial Catalog=vault;Persist Security Info=False;User ID=sa;Password=$databasePassword;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=True;Connection Timeout=30; -globalSettings:identityServer:certificatePassword=$certPassword -globalSettings:duo:aKey=$duoKey -globalSettings:yubico:clientId=REPLACE -globalSettings:yubico:REPLACE" - -Add-Content $dockerDir/mssql.override.env " -ACCEPT_EULA=Y -MSSQL_PID=Express -SA_PASSWORD=$databasePassword" +echo "Setup complete" diff --git a/scripts/setup.sh b/scripts/setup.sh index b447cbf3a..467b661cb 100644 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -7,10 +7,7 @@ echo -e "\nPlease enter your email address (used to generate an HTTPS certificat read EMAIL OUTPUT_DIR=./bitwarden -DOCKER_DIR=../docker -CERT_PASSWORD=$(LC_ALL=C tr -dc A-Za-z0-9 > $DOCKER_DIR/global.override.env << EOF -globalSettings:baseServiceUri:vault=https://$DOMAIN -globalSettings:baseServiceUri:api=https://$DOMAIN/api -globalSettings:baseServiceUri:identity=https://$DOMAIN/identity -globalSettings:sqlServer:connectionString=Server=tcp:mssql,1433;Initial Catalog=vault;Persist Security Info=False;User ID=sa;Password=$DATABASE_PASSWORD;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=True;Connection Timeout=30; -globalSettings:identityServer:certificatePassword=$CERT_PASSWORD -globalSettings:duo:aKey=$DUO_KEY -globalSettings:yubico:clientId=REPLACE -globalSettings:yubico:REPLACE -EOF - -cat >> $DOCKER_DIR/mssql.override.env << EOF -ACCEPT_EULA=Y -MSSQL_PID=Express -SA_PASSWORD=$DATABASE_PASSWORD -EOF \ No newline at end of file +echo -e "\nSetup complete" diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 6f06d4797..f7bc928cb 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -92,7 +92,7 @@ namespace Bit.Api // Services services.AddBaseServices(); - services.AddDefaultServices(); + services.AddDefaultServices(globalSettings); // Cors services.AddCors(config => diff --git a/src/Billing/Startup.cs b/src/Billing/Startup.cs index 9a6a47c25..e384955ca 100644 --- a/src/Billing/Startup.cs +++ b/src/Billing/Startup.cs @@ -45,7 +45,7 @@ namespace Bit.Billing // Services services.AddBaseServices(); - services.AddDefaultServices(); + services.AddDefaultServices(globalSettings); services.TryAddSingleton(); diff --git a/src/Core/Utilities/LoggerFactoryExtensions.cs b/src/Core/Utilities/LoggerFactoryExtensions.cs index b7de1979b..6e24f4061 100644 --- a/src/Core/Utilities/LoggerFactoryExtensions.cs +++ b/src/Core/Utilities/LoggerFactoryExtensions.cs @@ -22,13 +22,22 @@ namespace Bit.Core.Utilities filter = (e) => true; } - var serilog = new LoggerConfiguration() + var config = new LoggerConfiguration() .Enrich.FromLogContext() - .Filter.ByIncludingOnly(filter) - .WriteTo.AzureDocumentDB(new Uri(globalSettings.DocumentDb.Uri), globalSettings.DocumentDb.Key, - timeToLive: TimeSpan.FromDays(7)) - .CreateLogger(); + .Filter.ByIncludingOnly(filter); + if(globalSettings.DocumentDb != null && !string.IsNullOrWhiteSpace(globalSettings.DocumentDb.Uri) && + !string.IsNullOrWhiteSpace(globalSettings.DocumentDb.Key)) + { + config.WriteTo.AzureDocumentDB(new Uri(globalSettings.DocumentDb.Uri), globalSettings.DocumentDb.Key, + timeToLive: TimeSpan.FromDays(7)); + } + else + { + // local file sink + } + + var serilog = config.CreateLogger(); factory.AddSerilog(serilog); appLifetime.ApplicationStopped.Register(Log.CloseAndFlush); } diff --git a/src/Core/Utilities/ServiceCollectionExtensions.cs b/src/Core/Utilities/ServiceCollectionExtensions.cs index 62f57a8cc..bc8b25623 100644 --- a/src/Core/Utilities/ServiceCollectionExtensions.cs +++ b/src/Core/Utilities/ServiceCollectionExtensions.cs @@ -48,10 +48,19 @@ namespace Bit.Core.Utilities services.AddSingleton(); } - public static void AddDefaultServices(this IServiceCollection services) + public static void AddDefaultServices(this IServiceCollection services, GlobalSettings globalSettings) { services.AddSingleton(); - services.AddSingleton(); + + if(!string.IsNullOrWhiteSpace(globalSettings.Mail.SendGridApiKey)) + { + services.AddSingleton(); + } + else + { + services.AddSingleton(); + } + #if NET461 services.AddSingleton(); services.AddSingleton(); @@ -59,8 +68,23 @@ namespace Bit.Core.Utilities services.AddSingleton(); services.AddSingleton(); #endif - services.AddSingleton(); - services.AddSingleton(); + if(!string.IsNullOrWhiteSpace(globalSettings.Storage.ConnectionString)) + { + services.AddSingleton(); + } + else + { + services.AddSingleton(); + } + + if(!string.IsNullOrWhiteSpace(globalSettings.Attachment.ConnectionString)) + { + services.AddSingleton(); + } + else + { + services.AddSingleton(); + } } public static void AddNoopServices(this IServiceCollection services) @@ -147,14 +171,18 @@ namespace Bit.Core.Utilities else if(!string.IsNullOrWhiteSpace(globalSettings.IdentityServer.CertificatePassword) && System.IO.File.Exists("identity.pfx")) { - var identityServerCert = CoreHelpers.GetCertificate("identity.pfx", + var identityServerCert = CoreHelpers.GetCertificate("identity.pfx", globalSettings.IdentityServer.CertificatePassword); identityServerBuilder.AddSigningCredential(identityServerCert); } + else if(!string.IsNullOrWhiteSpace(globalSettings.IdentityServer.CertificateThumbprint)) + { + var identityServerCert = CoreHelpers.GetCertificate(globalSettings.IdentityServer.CertificateThumbprint); + identityServerBuilder.AddSigningCredential(identityServerCert); + } else { - var identityServerCert = CoreHelpers.GetCertificate(globalSettings.IdentityServer.CertificateThumbprint); - identityServerBuilder.AddSigningCredential(identityServerCert); + throw new Exception("No identity certificate to use."); } services.AddScoped(); @@ -168,7 +196,9 @@ namespace Bit.Core.Utilities this IServiceCollection services, IHostingEnvironment env, GlobalSettings globalSettings) { #if NET461 - if(!env.IsDevelopment() && !globalSettings.SelfHosted) + if(!env.IsDevelopment() && !globalSettings.SelfHosted && + !string.IsNullOrWhiteSpace(globalSettings.Storage.ConnectionString) && + !string.IsNullOrWhiteSpace(globalSettings.DataProtection.CertificateThumbprint)) { var dataProtectionCert = CoreHelpers.GetCertificate(globalSettings.DataProtection.CertificateThumbprint); var storageAccount = CloudStorageAccount.Parse(globalSettings.Storage.ConnectionString); diff --git a/src/Identity/Startup.cs b/src/Identity/Startup.cs index e6261e105..886dc2c64 100644 --- a/src/Identity/Startup.cs +++ b/src/Identity/Startup.cs @@ -49,7 +49,7 @@ namespace Bit.Identity // Services services.AddBaseServices(); - services.AddDefaultServices(); + services.AddDefaultServices(globalSettings); } public void Configure( diff --git a/src/Identity/entrypoint.sh b/src/Identity/entrypoint.sh index 8b1f828c0..7359bfb8f 100644 --- a/src/Identity/entrypoint.sh +++ b/src/Identity/entrypoint.sh @@ -1,5 +1,4 @@ #!/bin/sh -cp /etc/core/identity.pfx /app/identity.pfx - +cp /etc/bitwarden/identity/identity.pfx /app/identity.pfx dotnet /app/Identity.dll diff --git a/util/Setup/Dockerfile b/util/Setup/Dockerfile new file mode 100644 index 000000000..51cca202b --- /dev/null +++ b/util/Setup/Dockerfile @@ -0,0 +1,10 @@ +FROM microsoft/dotnet:2.0.0-preview2-runtime + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ +# Dependencies + openssl \ +&& rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY obj/Docker/publish . diff --git a/util/Setup/Helpers.cs b/util/Setup/Helpers.cs new file mode 100644 index 000000000..7e07acd09 --- /dev/null +++ b/util/Setup/Helpers.cs @@ -0,0 +1,91 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace Setup +{ + public static class Helpers + { + public static string SecureRandomString(int length, bool alpha = true, bool upper = true, bool lower = true, + bool numeric = true, bool special = false) + { + return SecureRandomString(length, RandomStringCharacters(alpha, upper, lower, numeric, special)); + } + + // ref https://stackoverflow.com/a/8996788/1090359 with modifications + public static string SecureRandomString(int length, string characters) + { + if(length < 0) + { + throw new ArgumentOutOfRangeException(nameof(length), "length cannot be less than zero."); + } + + if((characters?.Length ?? 0) == 0) + { + throw new ArgumentOutOfRangeException(nameof(characters), "characters invalid."); + } + + const int byteSize = 0x100; + if(byteSize < characters.Length) + { + throw new ArgumentException( + string.Format("{0} may contain no more than {1} characters.", nameof(characters), byteSize), + nameof(characters)); + } + + var outOfRangeStart = byteSize - (byteSize % characters.Length); + using(var rng = RandomNumberGenerator.Create()) + { + var sb = new StringBuilder(); + var buffer = new byte[128]; + while(sb.Length < length) + { + rng.GetBytes(buffer); + for(var i = 0; i < buffer.Length && sb.Length < length; ++i) + { + // Divide the byte into charSet-sized groups. If the random value falls into the last group and the + // last group is too small to choose from the entire allowedCharSet, ignore the value in order to + // avoid biasing the result. + if(outOfRangeStart <= buffer[i]) + { + continue; + } + + sb.Append(characters[buffer[i] % characters.Length]); + } + } + + return sb.ToString(); + } + } + + private static string RandomStringCharacters(bool alpha, bool upper, bool lower, bool numeric, bool special) + { + var characters = string.Empty; + if(alpha) + { + if(upper) + { + characters += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + } + + if(lower) + { + characters += "abcdefghijklmnopqrstuvwxyz"; + } + } + + if(numeric) + { + characters += "0123456789"; + } + + if(special) + { + characters += "!@#$%^*&"; + } + + return characters; + } + } +} diff --git a/util/Setup/Program.cs b/util/Setup/Program.cs new file mode 100644 index 000000000..1ad275ebd --- /dev/null +++ b/util/Setup/Program.cs @@ -0,0 +1,225 @@ +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 _parameters = null; + private static string _domain = null; + private static string _certPassword = null; + private static bool _ssl = false; + private static bool _letsEncrypt = false; + + public static void Main(string[] args) + { + _args = args; + _parameters = ParseParameters(); + + _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); + _certPassword = Helpers.SecureRandomString(32, alpha: true, numeric: true); + + MakeIdentityCert(); + BuildNginxConfig(); + BuildEnvironmentFiles(); + } + + private static void MakeIdentityCert() + { + Directory.CreateDirectory("/bitwarden/identity/"); + var identityCertResult = Exec("openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout identity.key " + + "-out identity.crt -subj \"/CN=bitwarden IdentityServer\" -days 10950"); + var identityPfxResult = Exec("openssl pkcs12 -export -out /bitwarden/identity/identity.pfx -inkey identity.key " + + $"-in identity.crt -certfile identity.crt -passout pass:{_certPassword}"); + } + + 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 || + (_parameters.ContainsKey("ssl_dh") ? _parameters["ssl_dh"].ToLowerInvariant() == "y" : false); + var trusted = _letsEncrypt || + (_parameters.ContainsKey("ssl_trusted") ? _parameters["ssl_trusted"].ToLowerInvariant() == "y" : false); + var certPath = _letsEncrypt ? $"/etc/letsencrypt/live/{_domain}" : $"/etc/certificates/{_domain}"; + + 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 {certPath}/fullchain.pem; + ssl_certificate_key {certPath}/privkey.pem; + + 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 {certPath}/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 {certPath}/fullchain.pem; + + resolver 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=300s;"); + } + + sw.WriteLine($@" + # Headers + + # 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 will enforce HTTP browsing into HTTPS and avoid ssl stripping attack + #add_header Strict-Transport-Security max-age=15768000;"); + } + + sw.WriteLine($@" + 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() + { + var url = _ssl ? $"https://{_domain}" : $"http://{_domain}"; + 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/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={_certPassword} +globalSettings:duo:aKey={Helpers.SecureRandomString(32, alpha: true, numeric: true)} +globalSettings:yubico:clientId=REPLACE +globalSettings:yubico:REPLACE"); + } + + using(var sw = File.CreateText("/bitwarden/mssql.override.env")) + { + sw.Write($@"ACCEPT_EULA=Y +MSSQL_PID=Express +SA_PASSWORD={dbPass}"); + } + } + + private static IDictionary ParseParameters() + { + var dict = new Dictionary(); + 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; + } + } +} diff --git a/util/Setup/Setup.csproj b/util/Setup/Setup.csproj new file mode 100644 index 000000000..890854918 --- /dev/null +++ b/util/Setup/Setup.csproj @@ -0,0 +1,13 @@ + + + + Exe + netcoreapp2.0 + + + + full + true + + + diff --git a/util/Setup/build.ps1 b/util/Setup/build.ps1 new file mode 100644 index 000000000..3b89c7576 --- /dev/null +++ b/util/Setup/build.ps1 @@ -0,0 +1,11 @@ +$dir = Split-Path -Parent $MyInvocation.MyCommand.Path + +echo "`n# Building Setup" + +echo "`nBuilding app" +echo ".NET Core version $(dotnet --version)" +dotnet publish $dir\Setup.csproj -f netcoreapp2.0 -c "Release" -o $dir\obj\Docker\publish + +echo "`nBuilding docker image" +docker --version +docker build -t bitwarden/setup $dir\. diff --git a/util/Setup/build.sh b/util/Setup/build.sh new file mode 100644 index 000000000..75a432628 --- /dev/null +++ b/util/Setup/build.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -e + +DIR="$(dirname $(readlink -f $0))" + +echo -e "\n# Building Setup" + +echo -e "\nBuilding app" +echo -e ".NET Core version $(dotnet --version)" +dotnet publish $DIR/Setup.csproj -f netcoreapp2.0 -c "Release" -o $DIR/obj/Docker/publish + +echo -e "\nBuilding docker image" +docker --version +docker build -t bitwarden/setup $DIR/.