Home > Docs > adyxax.org > vaultwarden > Installation
Installation notes of vaultwarden on k3s
Tags: k3s kubernetes postgresql vaultwarden

Introduction

Please refer to the official website documentation for an up to date installation guide. This page only lists what I had to do at the time to setup pass and adapt it to my particular setup. I updated these instructions after migrating from a traditional hosting to kubernetes.

Preparing the postgresql database

I have a postgresql running in its own namespace from bitnami images. To provision the pass database I :

export POSTGRES_PASSWORD=$(k get secret -n postgresql postgresql-secrets -o jsonpath="{.data.postgresql-password}"|
    base64 --decode)
k run client --rm -ti -n postgresql --image docker.io/bitnami/postgresql:13.4.0-debian-10-r52 \
    --env="PGPASSWORD=$POSTGRES_PASSWORD" --command --  psql --host postgresql -U postgres
CREATE ROLE pass WITH LOGIN PASSWORD 'secret';
CREATE DATABASE pass WITH OWNER pass TEMPLATE template0 ENCODING UTF8 LC_COLLATE
    'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8';
\c pass
create extension hstore;

Optionally import a dump of the database by running in another shell :

k -n postgresql cp pass.sql-20211005 client:/tmp/

Then in the psql shell :

\c pass
\i /tmp/pass.sql-20211005

Kubernetes manifests in terraform

This app is part of an experiment of mine to migrate stuff from traditional hosting to kubernetes. I first wrote manifests by hand then imported them with terraform. I do not like it and find it too complex/overkill but that is managed this way for now.

DNS CNAME

Since all configuration regarding this application is in terraform, so is the dns :

resource "cloudflare_record" "pass-cname" {
  zone_id = lookup(data.cloudflare_zones.adyxax-org.zones[0], "id")
  name    = "pass"
  value   = "myth.adyxax.org"
  type    = "CNAME"
  proxied = false
}

Namespace

The basic terraform object works for simple things so here it is :

resource "kubernetes_namespace" "myth-pass" {
  provider = kubernetes.myth
  metadata {
    name = "pass"
  }
}

Secret

Here is the kubernetes secret that tells pass how to connect the database. The password comes from terraform.tfvars, you might need to update the service url with the format <svc>.<namespace>.svc.cluster.local :

resource "kubernetes_secret" "myth-pass-secrets" {
  provider = kubernetes.myth
  metadata {
    name      = "pass-secrets"
    namespace = kubernetes_namespace.myth-pass.id
  }
  data = {
    ADMIN_PASSWORD = var.pass-admin-password
    DATABASE_URL   = join("", [ "postgres://pass:${var.pass-postgres-password}",
        "@postgresql.postgresql.svc.cluster.local/pass?sslmode=disable"])
  }
  type = "Opaque"
}

Deployment

I could not write the deployment with the kubernetes_deployment terraform ressource, so it is a row manifest which imports a yaml syntax in hcl. It is horrible to look at but works. Change the image tag to the latest stable version of pass before deploying :

resource "kubernetes_manifest" "myth-deployment-pass" {
  provider = kubernetes.myth
  manifest = {
    "apiVersion" = "apps/v1"
    "kind"       = "Deployment"
    "metadata" = {
      "name"      = "pass"
      "namespace" = kubernetes_namespace.myth-pass.id
    }
    "spec" = {
      "replicas" = 1
      "selector" = {
        "matchLabels" = {
          "app" = "pass"
        }
      }
      "strategy" = {
        "type" = "RollingUpdate"
        "rollingUpdate" = {
          "maxSurge"       = 1
          "maxUnavailable" = 0
        }
      }
      "template" = {
        "metadata" = {
          "labels" = {
            "app" = "pass"
          }
        }
        "spec" = {
          "containers" = [
            {
              "env" = [
                {
                  "name" = "DATABASE_URL"
                  "valueFrom" = {
                    "secretKeyRef" = {
                      "key"  = "DATABASE_URL"
                      "name" = "pass-secrets"
                    }
                  }
                },
                {
                  "name"  = "RUN_MIGRATIONS"
                  "value" = "1"
                },
                {
                  "name"  = "ADMIN_USERNAME"
                  "value" = "admin"
                },
                {
                  "name" = "ADMIN_PASSWORD"
                  "valueFrom" = {
                    "secretKeyRef" = {
                      "key"  = "ADMIN_PASSWORD"
                      "name" = "pass-secrets"
                    }
                  }
                },
              ]
              "image" = "vaultwarden/server:1.23.0"
              "livenessProbe" = {
                "httpGet" = {
                  "path" = "/"
                  "port" = 8080
                }
                "initialDelaySeconds" = 5
                "timeoutSeconds"      = 5
              }
              "name" = "pass"
              "ports" = [
                {
                  "containerPort" = 8080
                },
              ]
              "readinessProbe" = {
                "httpGet" = {
                  "path" = "/"
                  "port" = 8080
                }
                "initialDelaySeconds" = 5
                "timeoutSeconds"      = 5
              }
              "lifecycle" = {
                "preStop" = {
                  "exec" = {
                    "command" = ["/bin/sh", "-c", "sleep 10"]
                  }
                }
              }
            },
          ]
          "terminationGracePeriodSeconds" = 1
        }
      }
    }
  }
}

Service

resource "kubernetes_manifest" "myth-service-pass" {
  provider = kubernetes.myth
  manifest = {
    "apiVersion" = "v1"
    "kind"       = "Service"
    "metadata" = {
      "name"      = "pass"
      "namespace" = kubernetes_namespace.myth-pass.id
    }
    "spec" = {
      "ports" = [
        {
          "port"       = 80
          "protocol"   = "TCP"
          "targetPort" = 8080
        },
      ]
      "selector" = {
        "app" = "pass"
      }
      "type" = "ClusterIP"
    }
  }
}

Ingress

resource "kubernetes_manifest" "myth-ingress-pass" {
  provider = kubernetes.myth
  manifest = {
    "apiVersion" = "networking.k8s.io/v1"
    "kind"       = "Ingress"
    "metadata" = {
      "name"      = "pass"
      "namespace" = kubernetes_namespace.myth-pass.id
    }
    "spec" = {
      "ingressClassName" = "nginx"
      "rules" = [
        {
          "host" = "pass.adyxax.org"
          "http" = {
            "paths" = [
              {
                "path"     = "/"
                "pathType" = "Prefix"
                "backend" = {
                  "service" = {
                    "name" = "pass"
                    "port" = {
                      "number" = 80
                    }
                  }
                }
              },
            ]
          }
        },
      ]
      "tls" = [
        {
          "secretName" = "wildcard-adyxax-org"
        },
      ]
    }
  }
}

Certificate

For now I do not manage my certificates with terraform but manually. Once every two months I run :

acme.sh --config-home "$HOME/.acme.sh" --server letsencrypt --dns dns_cf --issue -d adyxax.org -d *.adyxax.org --force
kubectl -n pass create secret tls wildcard-adyxax-org --cert=$HOME/.acme.sh/adyxax.org/fullchain.cer \
  --key=$HOME/.acme.sh/adyxax.org/adyxax.org.key -o yaml --save-config --dry-run=client | kubectl apply -f -