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/.