The purpose of this chart is to enable the deployment of Bitwarden to different Kubernetes environments. This chart is built for usage across multiple Kubernetes hosting scenarios.
- Kubectl
- Helm 3
- SSL cert and key or certificate provider
- SMTP server/account
- Storage Class that supports ReadWriteMany
helm repo add bitwarden https://charts.bitwarden.com/
helm repo update
- Request an installation ID and key from: https://bitwarden.com/host/
Run the following command to create a custom values file used for deployment:
helm show values bitwarden/self-host > my-values.yaml
Edit the my-values.yaml
file and fill out the values. Required values that must be set:
- general.domain
- general.ingress.enabled (set to disbled if you are creating your own ingress)
- general.ingress.className (nginx example provided)
- general.ingress.annotations (nginx example provided)
- general.ingress.paths (nginx example provided)
- general.ingress.cert.tls.name
- general.email.replyToEmail
- general.email.smtpHost
- general.emal.smtpPort
- general.email.smtpSsl
- sharedStorageClassName
- database.enabled (set to disbled if using an external SQL server)
Note that default values for Nginx have been setup for the ingress in the values.yaml file. Some other ingress controller examples are provided later in this document.
The SCIM pod is disabled by default. To enable the SCIM pod, set component.scim.enabled
in my-values.yaml
to true
.
- Create a namespace to deploy Bitwarden to. In this guide, we will be using
bitwarden
as the namespace.- Run
kubectl create namespace bitwarden
.
- Run
Create a secret to set the following values.
- globalSettings__installation__id
- globalSettings__installation__key
- globalSettings__mail__smtp__username
- globalSettings__mail__smtp__password
- globalSettings__yubico__clientId
- globalSettings__yubico__key
- globalSettings__hibpApiKey
- SA_PASSWORD (if using the Bitwarden SQL pod)
- globalSettings__sqlServer__connectionString (if using your own SQL server)
Here we document the process of creating the secret using the command line. However, you can also use a CSI secret provider class, which we document an example of under "Installing the Azure Key Vault CSI Driver" later in this README.
Examples of kubectl secret creation are provided below. One is for use with SQL deployed in a pod. The other is for usage with an external SQL server.
-
With included SQL pod
kubectl create secret generic custom-secret -n bitwarden\ --from-literal=globalSettings__installation__id="REPLACE" \ --from-literal=globalSettings__installation__key="REPLACE" \ --from-literal=globalSettings__mail__smtp__username="REPLACE" \ --from-literal=globalSettings__mail__smtp__password="REPLACE" \ --from-literal=globalSettings__yubico__clientId="REPLACE" \ --from-literal=globalSettings__yubico__key="REPLACE" \ --from-literal=globalSettings__hibpApiKey="REPLACE" \ --from-literal=SA_PASSWORD="REPLACE"
-
With external SQL server
kubectl create secret generic custom-secret -n bitwarden\ --from-literal=globalSettings__installation__id="REPLACE" \ --from-literal=globalSettings__installation__key="REPLACE" \ --from-literal=globalSettings__mail__smtp__username="REPLACE" \ --from-literal=globalSettings__mail__smtp__password="REPLACE" \ --from-literal=globalSettings__sqlServer__connectionString="Data Source=tcp:<SERVERNAME>,1433;Initial Catalog=vault;Persist Security Info=False;User ID=<USER>;Password=<PASSWORD>;Multiple Active Result Sets=False;Connect Timeout=30;Encrypt=True;Trust Server Certificate=True" \ --from-literal=globalSettings__yubico__clientId="REPLACE" \ --from-literal=globalSettings__yubico__key="REPLACE" \ --from-literal=globalSettings__hibpApiKey="REPLACE" \
NOTE: These commands are recorded in your shell history. To avoid this, consider setting up a CSI secret provider class.
Set secrets.secretName
to the name of the secret created above.
Replace any optional values in my-values.yaml
to best fit your cluster. This includes changing of resource limits and requests.
This chart allows you to include other Kubernetes manifest files either pre- or post-install. To do this, update the rawManifests
section of the chart
rawManifests:
preInstall: []
postInstall: []
The example below shows how you can use the raw manifests to install Traefik's IngressRoute instead of using the Kubernetes Ingress controller. Note that you will want to disable the ingress controller under general.ingress.enabled
to use this.
rawManifests:
preInstall: []
postInstall:
- apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: "bitwarden-self-host-middleware-stripprefix"
spec:
stripPrefix:
prefixes:
- /api
- /attachements
- /icons
- /notifications
- /events
- /scim
##### NOTE: Admin, Identity, and SSO will not function correctly with path strip middleware
- apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: "bitwarden-self-host-ingress"
spec:
entryPoints:
- websecure
routes:
- kind: Rule
match: Host(`REPLACEME.COM`) && PathPrefix(`/`)
services:
- kind: Service
name: bitwarden-self-host-web
passHostHeader: true
port: 5000
- kind: Rule
match: Host(`REPLACEME.COM`) && PathPrefix(`/api/`)
services:
- kind: Service
name: bitwarden-self-host-api
port: 5000
middlewares:
- name: "bitwarden-self-host-middleware-stripprefix"
- kind: Rule
match: Host(`REPLACEME.COM`) && PathPrefix(`/attachments/`)
services:
- kind: Service
name: bitwarden-self-host-api
port: 5000
middlewares:
- name: "bitwarden-self-host-middleware-stripprefix"
- kind: Rule
match: Host(`REPLACEME.COM`) && PathPrefix(`/icons/`)
services:
- kind: Service
name: bitwarden-self-host-icons
port: 5000
middlewares:
- name: "bitwarden-self-host-middleware-stripprefix"
- kind: Rule
match: Host(`REPLACEME.COM`) && PathPrefix(`/notifications/`)
services:
- kind: Service
name: bitwarden-self-host-notifications
port: 5000
middlewares:
- name: "bitwarden-self-host-middleware-stripprefix"
- kind: Rule
match: Host(`REPLACEME.COM`) && PathPrefix(`/events/`)
services:
- kind: Service
name: bitwarden-self-host-events
port: 5000
middlewares:
- name: "bitwarden-self-host-middleware-stripprefix"
- kind: Rule
match: Host(`REPLACEME.COM`) && PathPrefix(`/scim/`)
services:
- kind: Service
name: bitwarden-self-host-scim
port: 5000
middlewares:
- name: "bitwarden-self-host-middleware-stripprefix"
##### NOTE: SSO will not function correctly with path strip middleware
- kind: Rule
match: Host(`REPLACEME.COM`) && PathPrefix(`/sso/`)
services:
- kind: Service
name: bitwarden-self-host-sso
port: 5000
##### NOTE: Identity will not function correctly with path strip middleware
- kind: Rule
match: Host(`REPLACEME.COM`) && PathPrefix(`/identity/`)
services:
- kind: Service
name: bitwarden-self-host-identity
port: 5000
##### NOTE: Admin will not function correctly with path strip middleware
- kind: Rule
match: Host(`REPLACEME.COM`) && PathPrefix(`/admin`)
services:
- kind: Service
name: bitwarden-self-host-admin
port: 5000
tls:
certResolver: letsencrypt
Note that the certResolver is deployed with the Traefik ingress configuration.
- Run
helm upgrade bitwarden bitwarden/self-host --install --namespace bitwarden --values my-values.yaml
.- This installs/upgrades a release named
bitwarden
, in the namespacebitwarden
, using values frommy-values.yaml
. - This may take over a minute to fully come up (some of the services might register as failed in the meantime)
- You can see help information for the
helm install
command by runninghelm install --help
. - You can see help information for the
helm upgrade
command by runninghelm upgrade --help
.
- This installs/upgrades a release named
The current Bitwarden release is architected in a way which requires the sharing of persistent data between containers and therefore requires storage which supports the access mode ReadWriteMany.
Edit values.yaml and update to suit your configuration.
Minimal required to get a running installation:
Below is an example of deploying this chart on AKS using various ingress controllers and cert-manager to provision the certificate from LetsEncrypt.
kubectl create ns bitwarden
This is the simplest ingress to setup and has been provided as the default. You will need the ingress controller installed if you have not already done so. Follow the basic configuration found at "Create an unmanaged ingress controller".
Then update the my-values.yaml file:
general:
# Domain name for the service
domain: "REPLACE"
ingress:
# Set to false if using a custom ingress
enabled: true
# Current supported values for ingress type include: nginx
className: "nginx"
## - Annotations to add to the Ingress resource
annotations:
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/use-regex: "true"
nginx.ingress.kubernetes.io/rewrite-target: /$1
These annotations can be used as-is.
Azure customers might want to use an Azure Application Gateway as the ingress controller for their AKS cluster. You will want to enable the Application Gateway ingress controller for your cluster before making these configuration changes.
Update the my-values.yaml file. Tweak the annotations as necessary for your environment.
general:
domain: "replaceme.com"
ingress:
enabled: true
className: "azure-application-gateway" # This value might be different depending on how you created your ingress controller. Use "kubectl get ingressclasses -A" to find the name if unsure
## - Annotations to add to the Ingress resource
annotations:
appgw.ingress.kubernetes.io/ssl-redirect: "true"
appgw.ingress.kubernetes.io/use-private-ip: "false" # This might be true depending on your setup
appgw.ingress.kubernetes.io/rewrite-rule-set: "bitwarden-ingress" # Make note of whatever you set this value to. It will be used later.
appgw.ingress.kubernetes.io/connection-draining: "true" # Update as necessary
appgw.ingress.kubernetes.io/connection-draining-timeout: "30" # Update as necessary
## - Labels to add to the Ingress resource
labels: {}
# Certificate options
tls:
# TLS certificate secret name
name: tls-secret
# Cluster cert issuer (ex. Let's Encrypt) name if one exists
clusterIssuer: letsencrypt-staging
paths:
web:
path: /*
pathType: ImplementationSpecific
attachments:
path: /attachments/*
pathType: ImplementationSpecific
api:
path: /api/*
pathType: ImplementationSpecific
icons:
path: /icons/*
pathType: ImplementationSpecific
notifications:
path: /notifications/*
pathType: ImplementationSpecific
events:
path: /events/*
pathType: ImplementationSpecific
scim:
path: /scim/*
pathType: ImplementationSpecific
sso:
path: /sso/*
pathType: ImplementationSpecific
identity:
path: /identity/*
pathType: ImplementationSpecific
admin:
path: /admin*
pathType: ImplementationSpecific
NOTE: Make sure to update the paths to what you see here.
Further settings will need to be set after the deployment on the Application Gateway itself.
If you have not done so, first install cert-manager on the cluster.
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.11.0/cert-manager.yaml
It is recommended to use the staging configuration of Let's Encrypt until your DNS records have been pointed correctly.
Use one of the following certificate issuers depending on if you are in a production or a pre-production environment:
cat <<EOF | kubectl apply -n bitwarden -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: me@example.com
privateKeySecretRef:
name: tls-secret
solvers:
- http01:
ingress:
class: nginx #use "azure/application-gateway" for Application Gateway ingress
EOF
cat <<EOF | kubectl apply -n bitwarden -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-production
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: me@example.com
privateKeySecretRef:
name: tls-secret
solvers:
- http01:
ingress:
class: nginx #use "azure/application-gateway" for Application Gateway ingress
EOF
Alternatively, you can create the cluster issuer via the rawManifets.preInstall
section of my-values.yaml
.
Finally, set the ingress TLS information in my-values.yaml
:
ingress:
...
# Certificate options
tls:
# TLS certificate secret name
name: tls-secret
# Cluster cert issuer (ex. Let's Encrypt) name if one exists
clusterIssuer: letsencrypt-staging
We will use the Azure File storage class for persistent storage:
cat <<EOF | kubectl apply -n bitwarden -f -
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: azure-file
namespace: bitwaren
provisioner: file.csi.azure.com
allowVolumeExpansion: true
mountOptions:
- dir_mode=0777
- file_mode=0777
- uid=0
- gid=0
- mfsymlinks
- cache=strict
- actimeo=30
parameters:
skuName: Standard_LRS
EOF
Alternatively, you can create the storage class via the rawManifets.preInstall
section of my-values.yaml
.
Set the sharedStorageClassName
value in my-values.yaml
to match the name provided.
sharedStorageClassName: "azure-file"
See the configuration sections above for required values. Secrets can be configured using the standard CLI method already provided. However, you can also use Azure Key Vault as the source for your secrets. This is optional but recommended.
The following will add the Azure Key Vault CSI driver to an existing cluster. More information can be found in this article: Use the Azure Key Vault Provider for Secrets Store CSI Driver in an Azure Kubernetes Service (AKS) cluster
az aks enable-addons --addons azure-keyvault-secrets-provider --name REPLACE --resource-group REPLACE
You will also want to configure identity access for your cluster to the Key Vault. This article provides a couple of different options: Provide an identity to access the Azure Key Vault Provider for Secrets Store CSI Driver in Azure Kubernetes Service (AKS)
Once the cluster identity has been granted access to the Key Vault, you will need to create a SecretProviderClass. An example is provided below.
cat <<EOF | kubectl apply -n bitwarden -f -
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: bitwarden-azure-keyvault-csi
labels:
app.kubernetes.io/component: secrets
annotations:
spec:
provider: azure
parameters:
useVMManagedIdentity: "true" # Set to false for workload identity
userAssignedIdentityID: "<REPLACE>" # Set the clientID of the user-assigned managed identity to use
# clientID: "<REPLACE>" # Setting this to use workload identity
keyvaultName: "<REPLACE>"
cloudName: "AzurePublicCloud"
objects: |
array:
- |
objectName: installationid
objectAlias: installationid
objectType: secret
objectVersion: ""
- |
objectName: installationkey
objectAlias: installationkey
objectType: secret
objectVersion: ""
- |
objectName: smtpusername
objectAlias: smtpusername
objectType: secret
objectVersion: ""
- |
objectName: smtppassword
objectAlias: smtppassword
objectType: secret
objectVersion: ""
- |
objectName: yubicoclientid
objectAlias: yubicoclientid
objectType: secret
objectVersion: ""
- |
objectName: yubicokey
objectAlias: yubicokey
objectType: secret
objectVersion: ""
- |
objectName: hibpapikey
objectAlias: hibpapikey
objectType: secret
objectVersion: ""
- |
objectName: sapassowrd #-OR- dbconnectionstring if external SQL
objectAlias: sapassowrd #-OR- dbconnectionstring if external SQL
objectType: secret
objectVersion: ""
tenantId: "<REPLACE>"
secretObjects:
- secretName: "bitwarden-secret"
type: Opaque
data:
- objectName: installationid
key: globalSettings__installation__id
- objectName: installationkey
key: globalSettings__installation__key
key: globalSettings__mail__smtp__username
- objectName: smtppassword
key: globalSettings__mail__smtp__password
- objectName: yubicoclientid
key: globalSettings__yubico__clientId
- objectName: yubicokey
key: globalSettings__yubico__key
- objectName: hibpapikey
key: globalSettings__hibpApiKey
- objectName: sapassowrd #-OR- dbconnectionstring if external SQL
key: SA_PASSWORD #-OR- globalSettings__sqlServer__connectionString if external SQL
EOF
Alternatively, you can create the secrets provider via the rawManifets.preInstall
section of my-values.yaml
.
Note the spots in the definition that say "<REPLACE>"
. These will need to be updated for your environment. Also note that you will again have the choice between using the SQL Server Pod and an external SQL Server. Those spots that will need to change have been marked with a comment. Finally, you can name the secrets in Azure Key Vault based on your own naming convention. If you do so, you must make certain that to update the objectName properties under spec.parameters.objects.array
to match the secrets created in Key Vault.
The following commands would create these secrts in a Key Vault:
kvname="kv-aks-bw-helm-cus-01"
az keyvault secret set --name installationid --vault-name $kvname --value <REPLACEME>
az keyvault secret set --name installationkey --vault-name $kvname --value <REPLACEME>
az keyvault secret set --name smtpusername --vault-name $kvname --value <REPLACEME>
az keyvault secret set --name smtppassword --vault-name $kvname --value <REPLACEME>
az keyvault secret set --name yubicoclientid --vault-name $kvname --value <REPLACEME>
az keyvault secret set --name yubicokey --vault-name $kvname --value <REPLACEME>
az keyvault secret set --name hibpapikey --vault-name $kvname --value <REPLACEME>
az keyvault secret set --name sapassword --vault-name $kvname --value '"<REPLACEME>"'
# - OR -
# az keyvault secret set --name dbconnectionstring --vault-name $kvname --value '"<REPLACEME>"'
NOTE: These values will be stored in your shell history. There are many other ways to set Key Vault secrets that are outside of the scope of this document. This provides you with one option.
Now, edit my-values.yaml
to use this secret provider class we created.
secrets:
secretName: bitwarden-secret # spec.secretObjects.secretName in example
secretProviderClass: bitwarden-azure-keyvault-csi #metadata.name in example
Application Gateway ingress deployments have a few more required steps for Bitwarden to function correctly. If you are using another ingress controller, you may skip to the next section.
We will need to create a rewrite set on the Application Gateway. There are various ways of doing this, but we will discuss using the Azure Portal. For now we are creating an empty set for the Helm deployment to work. We will add the rewrite rule after deploying Helm.
- Navigate to the Application Gateway in the Azure Portal
- Once in the Application Gateway, find the "Rewrites" blade in the left-hand navigation menu.
- Click the "+ Rewrite set" button at the top of the main page section to add a new rewrite set
- On the "Update rewrite set" page in the "Name and Association" tab set the
Name
field to the same value specified in theappgw.ingress.kubernetes.io/rewrite-rule-set
ingress annotation - Click Next
- Click Create
helm upgrade bitwarden bitwarden/self-host --install --namespace bitwarden --values my-values.yaml
Application Gateway ingress deployments have one more required step for Bitwarden to function correctly. If you are using another ingress controller, you may skip to the next section.
We will need to finish the rewrite set on the Application Gateway we created earlier.
- Reopen the rewrite set you created earlier.
- On the "Update rewrite set" page in the "Name and Association" tab, select all routing paths that begin with pr-bitwarden-self-host-ingress... , deselect any that do not begin with that prefix, and then select Next.
- On the "Rewrite rule configuration" tab, click the "Add rewrite rule" button.
- Enter a name for the rule. This can be anything that helps you with organization. Something similar to "bitwarden-rewrite" will work.
- The rule sequence value does not matter for this purpose.
- Add a condition and set the following values:
- Type of variable to check: Server variable
- Server variable: uri_path
- Case-sensitive: No
- Operator: equal (=)
- Pattern to match:
^(\/(?!admin)(?!identity)(?!sso)[^\/]*)\/(.*)
- Click OK
- Add an action and set the following values:
- Rewrite type: URL
- Action type: Set
- Components: URL path
- URL path value:
/{var_uri_path_2}
- Re-evalueate path map: Unchecked
- Click OK
- Click "Update" at the bottom of the screen.
You can find the public IP to point your DNS record at by running:
kubectl get ingress -n bitwarden
The public IP will be found on the Overview tab of the Application Gateway service in the Azure Portal.
This section will walk through an example of hosting Bitwarden on OpenShift. Note that there are many different permutations of how you can host Bitwarden on this platform. We will provide some basic pointers.
Run the following shell commands to create a project in OpenShift.
oc new-project bitwarden
oc project bitwarden
We will use OpenShift Routes for our ingress in this example. Alternatively, ingress operators could be used.
In your my-values.yaml
, disable the default ingress.
general:
domain: "replaceme.com"
ingress:
enabled: false
You can ignore the rest of the ingress section.
Update the rawManifests
section in my-values.yaml
to include OpenShift Route manifests.
rawManifests:
preInstall: []
postInstall:
- kind: Route
apiVersion: route.openshift.io/v1
metadata:
name: bitwarden-self-host-web
namespace: bitwarden
annotations:
haproxy.router.openshift.io/rewrite-target: /
spec:
host: bitwarden.apps-crc.testing
path: "/"
to:
kind: Service
name: bitwarden-self-host-web
weight: 100
port:
targetPort: 5000
tls:
termination: edge
insecureEdgeTerminationPolicy: Redirect
destinationCACertificate: ''
- kind: Route
apiVersion: route.openshift.io/v1
metadata:
name: bitwarden-self-host-api
namespace: bitwarden
annotations:
haproxy.router.openshift.io/rewrite-target: /
spec:
host: bitwarden.apps-crc.testing
path: "/api/"
to:
kind: Service
name: bitwarden-self-host-api
weight: 100
port:
targetPort: 5000
tls:
termination: edge
insecureEdgeTerminationPolicy: Redirect
destinationCACertificate: ''
- kind: Route
apiVersion: route.openshift.io/v1
metadata:
name: bitwarden-self-host-attachments
namespace: bitwarden
annotations:
haproxy.router.openshift.io/rewrite-target: /
spec:
host: bitwarden.apps-crc.testing
path: "/attachments/"
to:
kind: Service
name: bitwarden-self-host-attachments
weight: 100
port:
targetPort: 5000
tls:
termination: edge
insecureEdgeTerminationPolicy: Redirect
destinationCACertificate: ''
- kind: Route
apiVersion: route.openshift.io/v1
metadata:
name: bitwarden-self-host-icons
namespace: bitwarden
annotations:
haproxy.router.openshift.io/rewrite-target: /
spec:
host: bitwarden.apps-crc.testing
path: "/icons/"
to:
kind: Service
name: bitwarden-self-host-icons
weight: 100
port:
targetPort: 5000
tls:
termination: edge
insecureEdgeTerminationPolicy: Redirect
destinationCACertificate: ''
- kind: Route
apiVersion: route.openshift.io/v1
metadata:
name: bitwarden-self-host-notifications
namespace: bitwarden
annotations:
haproxy.router.openshift.io/rewrite-target: /
spec:
host: bitwarden.apps-crc.testing
path: "/notifications/"
to:
kind: Service
name: bitwarden-self-host-notifications
weight: 100
port:
targetPort: 5000
tls:
termination: edge
insecureEdgeTerminationPolicy: Redirect
destinationCACertificate: ''
- kind: Route
apiVersion: route.openshift.io/v1
metadata:
name: bitwarden-self-host-events
namespace: bitwarden
annotations:
haproxy.router.openshift.io/rewrite-target: /
spec:
host: bitwarden.apps-crc.testing
path: "/events/"
to:
kind: Service
name: bitwarden-self-host-events
weight: 100
port:
targetPort: 5000
tls:
termination: edge
insecureEdgeTerminationPolicy: Redirect
destinationCACertificate: ''
- kind: Route
apiVersion: route.openshift.io/v1
metadata:
name: bitwarden-self-host-scim
namespace: bitwarden
annotations:
haproxy.router.openshift.io/rewrite-target: /
spec:
host: bitwarden.apps-crc.testing
path: "/scim/"
to:
kind: Service
name: bitwarden-self-host-scim
weight: 100
port:
targetPort: 5000
tls:
termination: edge
insecureEdgeTerminationPolicy: Redirect
destinationCACertificate: ''
- kind: Route
apiVersion: route.openshift.io/v1
metadata:
name: bitwarden-self-host-sso
namespace: bitwarden
annotations:
# Rewrite will not work with sso
spec:
host: bitwarden.apps-crc.testing
path: "/sso/"
to:
kind: Service
name: bitwarden-self-host-sso
weight: 100
port:
targetPort: 5000
tls:
termination: edge
insecureEdgeTerminationPolicy: Redirect
destinationCACertificate: ''
- kind: Route
apiVersion: route.openshift.io/v1
metadata:
name: bitwarden-self-host-identity
namespace: bitwarden
annotations:
# Rewrite will not work with identity
spec:
host: bitwarden.apps-crc.testing
path: "/identity/"
to:
kind: Service
name: bitwarden-self-host-identity
weight: 100
port:
targetPort: 5000
tls:
termination: edge
insecureEdgeTerminationPolicy: Redirect
destinationCACertificate: ''
- kind: Route
apiVersion: route.openshift.io/v1
metadata:
name: bitwarden-self-host-admin
namespace: bitwarden
annotations:
# Rewrite will not work with admin
spec:
host: bitwarden.apps-crc.testing
path: "/admin"
to:
kind: Service
name: bitwarden-self-host-admin
weight: 100
port:
targetPort: 5000
tls:
termination: edge
insecureEdgeTerminationPolicy: Redirect
destinationCACertificate: ''
Note that in this example we are setting destinationCACertificate
to an empty string. This will use the default certificate setup in OpenShift. Alternatively, specify a certificate name here, or you can use Let's Encrypt by following this guide: Secure Red Had OpenShift routs with Let's Encrypt. If you do so, you will need to add kubernetes.io/tls-acme: "true"
to the annotations for each route.
A shared storage class will be required. As stated earlier in the document, a ReadWriteMany-capable Storage Class will need to be set up. There are several options for this in OpenShift. One viable option is to use the NFS Subdir External Provisioner.
Using a secrets provider HashiCorp Vault is valid, but we will focus on using the oc
command to deploy our secrets. The process is similar to using kubectl
detailed earlier.
oc create secret generic custom-secret -n bitwarden \
--from-literal=globalSettings__installation__id="REPLACE" \
--from-literal=globalSettings__installation__key="REPLACE" \
--from-literal=globalSettings__mail__smtp__username="REPLACE" \
--from-literal=globalSettings__mail__smtp__password="REPLACE" \
--from-literal=globalSettings__yubico__clientId="REPLACE" \
--from-literal=globalSettings__yubico__key="REPLACE" \
--from-literal=globalSettings__hibpApiKey="REPLACE" \
--from-literal=SA_PASSWORD="REPLACE" # If using SQL pod
# --from-literal=globalSettings__sqlServer__connectionString="REPLACE" # If using your own SQL server
Bitwarden currently requires the use of a service account in OpenShift due to each container's need to run elevated commands on start-up. These commands are blocked by OpenShift's restricted SCCs. We need to create a service account and assign it to the anyuid
SCC.
oc create sa bitwarden-sa
oc adm policy add-scc-to-user anyuid -z bitwarden-sa
Next, update my-values.yaml
to use this service account. Note that this is a different service account from the one in the serviceAccount
section of the values YAML file. Instead, set the following keys to the name of the service account created:
- component.admin.podServiceAccount
- component.api.podServiceAccount
- component.attachments.podServiceAccount
- component.events.podServiceAccount
- component.icons.podServiceAccount
- component.identity.podServiceAccount
- component.notifications.podServiceAccount
- component.scim.podServiceAccount
- component.sso.podServiceAccount
- component.web.podServiceAccount
- database.podServiceAccount
component:
# The Admin component
admin:
# Additional deployment labels
labels: {}
# Image name, tag, and pull policy
image:
name: bitwarden/admin
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
securityContext:
podServiceAccount: bitwarden-sa
NOTE: You can create your own SSC to fine-tune the security of these pods. Managing SSCs in OpenShift describes the out-of-the-box SSCs and how to create your own if desired.
Update the other settings in my-values.yaml
based on your environment. Follow the instructions earlier in this document for required settings to update.
helm upgrade bitwarden bitwarden/self-host --install --namespace bitwarden --values my-values.yaml
This section will walk through an example of hosting Bitwarden on AWS EKS. This is just one possible way to deploy the chart as there are many ways of handling persistent storage, secrets, and ingress.
Follow the instructions above for creating the namespace.
The ALB ingress is not currently recommended since it does not support path rewrites with path-based routing. This example uses Nginx, but Traefik could also be used here.
The ingress controller will set up an AWS Network Load Balancer. Below, we define certain annotations that you should consider setting on the ingress controller service. The specific settings will be dependent on your environment. In this example, we are setting the SSL certificate on the load balancer instead of using Let's Encrypt. These annotations specify the certificate provided by AWS Certificate Manager using the certificate's ARN in the aws-load-balancer-ssl-cert
annotation.
- service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "ssl"
- service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
- service.beta.kubernetes.io/aws-load-balancer-type: "external"
- service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "instance"
- service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
- service.beta.kubernetes.io/aws-load-balancer-ssl-cert: "arn:aws:acm:REPLACEME:REPLACEME:certificate/REPLACEME" # ARN for the certificate
- service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "443"
You will also want to set the spec.externalTrafficPolicy
property to "Local" on the service. The following script will install the controller with these settings:
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm upgrade -i ingress-nginx ingress-nginx/ingress-nginx \
--namespace kube-system \
--set-string controller.service.annotations.'service\.beta\.kubernetes\.io/aws-load-balancer-backend-protocol'="ssl" \
--set-string controller.service.annotations.'service\.beta\.kubernetes\.io/aws-load-balancer-cross-zone-load-balancing-enabled'="true" \
--set-string controller.service.annotations.'service\.beta\.kubernetes\.io/aws-load-balancer-type'="external" \
--set-string controller.service.annotations.'service\.beta\.kubernetes\.io/aws-load-balancer-nlb-target-type'="instance" \
--set-string controller.service.annotations.'service\.beta\.kubernetes\.io/aws-load-balancer-scheme'="internet-facing" \
--set-string controller.service.annotations.'service\.beta\.kubernetes\.io/aws-load-balancer-ssl-cert'="arn:aws:acm:REPLACEME:REPLACEME:certificate/REPLACEME" \ #Replace with the ARN for your certificate
--set-string controller.service.annotations.'service\.beta\.kubernetes\.io/aws-load-balancer-ssl-ports'="443" \
--set controller.service.externalTrafficPolicy=Local
The following settings will create an Nginx ingress. These settings are specific to the Nginx ingress controller annotations we detailed earlier.
general:
domain: "REPLACEME.com"
ingress:
enabled: true
className: "nginx"
## - Annotations to add to the Ingress resource
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/use-regex: "true"
nginx.ingress.kubernetes.io/rewrite-target: /$1
## - Labels to add to the Ingress resource
labels: {}
# Certificate options
tls:
# TLS certificate secret name
name: # Handled via the NLB defined in the ingress controller
# Cluster cert issuer (ex. Let's Encrypt) name if one exists
clusterIssuer:
paths:
web:
path: /(.*)
pathType: ImplementationSpecific
attachments:
path: /attachments/(.*)
pathType: ImplementationSpecific
api:
path: /api/(.*)
pathType: ImplementationSpecific
icons:
path: /icons/(.*)
pathType: ImplementationSpecific
notifications:
path: /notifications/(.*)
pathType: ImplementationSpecific
events:
path: /events/(.*)
pathType: ImplementationSpecific
scim:
path: /scim/(.*)
pathType: ImplementationSpecific
sso:
path: /(sso/.*)
pathType: ImplementationSpecific
identity:
path: /(identity/.*)
pathType: ImplementationSpecific
admin:
path: /(admin/?.*)
pathType: ImplementationSpecific
To use EFS persistent storage, you will need to set up the Amazon EFS CSI driver. To do so, please follow the Amazon EFS CSI driver documentation. After the driver has been set up, you will need to create a storage class. The exact settings on the storage class will be different for every cluster, but an example is provided below.
file_system_id="REPLACE ME"
cat << EOF | kubectl apply -n bitwarden -f -
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: shared-storage
provisioner: efs.csi.aws.com
parameters:
provisioningMode: efs-ap
fileSystemId: $file_system_id
directoryPerms: "777" # Change for your use case
uid: "2000" # Change for your use case
gid: "2000" # Change for your use case
basePath: "/dyn1"
subPathPattern: "\${.PVC.name}"
ensureUniqueDirectory: "false"
reuseAccessPoint: "false"
mountOptions:
- iam
- tls
EOF
Alternatively, you can create the storage provider via the rawManifets.preInstall
section of my-values.yaml
.
Review the CSI Driver for Amazon EFS GitHub page for further information on these settings. After the storage class has been created, set the storage class name in my-values.yaml
:
sharedStorageClassName: shared-storage
We have detailed several secret provider options. For this example, we will use AWS Secrets Manager.
First, create a secret in AWS Secrets Manager. You will want to create a secret with keys similar to those below. If you use different key names, make sure to update those in the secret provider class we create later.
- installationid
- installationkey
- smtpusername
- smtppassword
- yubicoclientid
- yubicokey
- sapassowrd OR dbconnectionstring if using external SQL
Follow Use AWS Secrets Manager secrets in Amazon Elastic Kubernetes Services to set up the driver and permissions. When creating the IAM permissions policy, one similar to the one below will suffice. Replace the "Resource" value with the ARN of your secret in Resource manager.
{
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Action": [
"secretsmanager:DescribeSecret",
"secretsmanager:GetSecretValue"
],
"Resource": "arn:aws:secretsmanager:REPLACEME:REPLACEME:secret:REPLACEME"
}
}
Create a service account, and give it access to your secret using the policy you created:
CLUSTER_NAME=replace_me
ACCOUNT_ID=111111111111 # replace with your AWS account ID
ROLE_NAME=replaceme # name of the role that will be created in IAM
POLICY_NAME=replaceme # the name of the policy you created earlier
eksctl create iamserviceaccount \
--cluster=$CLUSTER_NAME \
--namespace=bitwarden \
--name=bitwarden-sa \
--role-name $ROLE_NAME \
--attach-policy-arn=arn:aws:iam::$ACCOUNT_ID:policy/$POLICY_NAME \
--approve
Next, create the secret provider class. The example below demonstrates how to do so. Make sure to update the region
and objectName
before deploying. If you used different keys when creating the secret in Secrets Manager, you will want to update the paths for the secrets as well.
cat <<EOF | kubectl apply -n bitwarden -f -
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: bitwarden-secrets-manager-csi
labels:
app.kubernetes.io/component: secrets
annotations:
spec:
provider: aws
parameters:
region: REPLACEME
objects: |
- objectName: "REPLACEME"
objectType: "secretsmanager"
objectVersionLabel: "AWSCURRENT"
jmesPath:
- path: installationid
objectAlias: installationid
- path: installationkey
objectAlias: installationkey
- path: smtpusername
objectAlias: smtpusername
- path: smtppassword
objectAlias: smtppassword
- path: yubicoclientid
objectAlias: yubicoclientid
- path: yubicokey
objectAlias: yubicokey
- path: hibpapikey
objectAlias: hibpapikey
- path: sapassword #-OR- dbconnectionstring if external SQL
objectAlias: sapassword #-OR- dbconnectionstring if external SQL
secretObjects:
- secretName: "bitwarden-secret"
type: Opaque
data:
- objectName: installationid
key: globalSettings__installation__id
- objectName: installationkey
key: globalSettings__installation__key
- objectName: smtpusername
key: globalSettings__mail__smtp__username
- objectName: smtppassword
key: globalSettings__mail__smtp__password
- objectName: yubicoclientid
key: globalSettings__yubico__clientId
- objectName: yubicokey
key: globalSettings__yubico__key
- objectName: hibpapikey
key: globalSettings__hibpApiKey
- objectName: sapassword #-OR- dbconnectionstring if external SQL
key: SA_PASSWORD #-OR- globalSettings__sqlServer__connectionString if external SQL
EOF
Alternatively, you can create the secrets provider via the rawManifets.preInstall
section of my-values.yaml
.
We now need to tell all of our pods to use the service account we created so that they can access the secrets. Update the serviceAccount
section in my-values.yaml
, and set the name of the service account created via eksctl
. Note that we set deployRolesOnly
to true
since we created the service account outside of our chart. The roles referenced grant the service account the ability to create secrets and get pod information inside the bitwarden
namespace. We used eksctl
to create the account instead of the Helm chart since we needed to grant IAM permissions for Secrets Manager access to the service account prior to deployment. The settings below tell the chart to create and assign the roles to that service account we already created.
#
# Configure service account for pre-install and post-install hooks
#
serviceAccount:
name: bitwarden-sa
# Certain instances will prequire the creation of a pre-deployed service account. For instance, AWS IAM enabled service accounts need to be created outside
# of the chart to allow for setting of permissions on other AWS services like Secrets Manager
deployRolesOnly: true
As the commented code above states, this only assigns the service account for the pre-install and post-install hooks. Our running pods will also need access to secrets. Update my-values.yaml
to use this same service account. Set the following keys to the name of the service account created:
- component.admin.podServiceAccount
- component.api.podServiceAccount
- component.attachments.podServiceAccount
- component.events.podServiceAccount
- component.icons.podServiceAccount
- component.identity.podServiceAccount
- component.notifications.podServiceAccount
- component.scim.podServiceAccount
- component.sso.podServiceAccount
- component.web.podServiceAccount
- database.podServiceAccount
See the example below:
component:
# The Admin component
admin:
# Additional deployment labels
labels: {}
# Image name, tag, and pull policy
image:
name: bitwarden/admin
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
securityContext:
podServiceAccount: bitwarden-sa
Note that you could use a separate service account created via eksctl
for the running pods. However, it will need the same IAM access policy assigned to it to access our secret in Secrets Manager. We have kept it simple and set the pods' and hooks' service accounts to the same account.
Finally, set the secrets section in my-values.yaml
with the information from our secret provider class we created.
secrets:
secretName: bitwarden-secret
secretProviderClass: bitwarden-secrets-manager-csi
Update the other settings in my-values.yaml
based on your environment. Follow the instructions earlier in this document for required settings to update.
helm upgrade bitwarden bitwarden/self-host --install --namespace bitwarden --values my-values.yaml
Please see the "examples" README for information on how you might setup database backups for the provided SQL pod.