Bitwarden Secrets Manager Setup¶
This repo loads all secrets and configuration from Bitwarden Secrets Manager at container start, instead of a plaintext .env. The only value stored locally is a Bitwarden access token.
Why¶
- Secret values never sit in a file on disk. The gitignored
.envholds just the access token. - One central store, rotatable and revocable, shared across machines.
bws runinjects each secret as an environment variable named exactly as the secret, so Terraform (TF_VAR_*,TF_TOKEN_app_terraform_io) and Ansible (lookup('env', ...)) work unchanged.
How it works¶
When you enter the container (task docker:exec), scripts/container/root/.bashrc sources the loader scripts/container/root/bws-load.sh, which:
- Reads
BWS_ACCESS_TOKEN(+BWS_PROJECT_ID) from the mounted/srv/.env. - Re-runs bash under
bws run --project-id <id> -- bash, which authenticates to Bitwarden cloud and injects every secret in the project as a native environment variable. - A
BWS_LOADEDguard prevents the re-run from recursing. Inside the injected shell the access token is unset, so it is not inherited by terraform/ansible or any subprocess.
If Bitwarden is unreachable or the token is bad, you still get a plain (secret-less) shell with a warning, not a lockout. BWS_SKIP=1 bypasses the auto-load entirely.
Reload without restarting. After you add or change a secret in Bitwarden, refresh the current shell in place, no need to exit the container:
For a one-off command with secrets injected (host or CI, non-interactive), wrap it with bws run:
Important: Bitwarden cloud is required. Vaultwarden does not implement Secrets Manager. The free tier covers this repo (unlimited secrets, 3 projects, 3 machine accounts).
1. Install the bws CLI locally¶
You need bws on your machine to create and manage secrets. There is no Homebrew formula; use the official install script (it detects platform/arch, downloads, and validates the checksum):
Pin a version with BWS_VERSION=2.1.0 curl -fsSL https://bws.bitwarden.com/install | sh. Or download a binary directly from the releases page (macOS: bws-macos-universal-*.zip; Linux: bws-<arch>-unknown-linux-musl-*.zip) and put it on PATH.
(The container already has bws baked into its image, so you only need it locally for management.)
2. Provision the backend (one-time)¶
The project homelab-infrastructure already exists. To (re)create the pieces on a fresh org:
- Enable Secrets Manager on your Bitwarden organization.
- Create a project, e.g.
homelab-infrastructure. Find its id withbws project list. - Create a machine account and grant it
can readon the project. - Create an access token for that machine account. Treat it like a password.
3. Configure your local .env¶
Copy the template and fill in only the two Bitwarden values (get the project id from bws project list):
cp .env.example .env
# then edit .env:
# BWS_ACCESS_TOKEN=<your machine-account access token>
# BWS_PROJECT_ID=<id from `bws project list`>
.env is gitignored. Neither value is ever committed.
Optional hardening (no token in any file): leave BWS_ACCESS_TOKEN out of .env, keep it in your host keychain/pass, and pass it in at run time with docker run -e BWS_ACCESS_TOKEN ....
4. Create the secrets¶
Load your ids first so the project id is never typed inline:
Web UI: Secrets Manager → project homelab-infrastructure → New secret. Name = the env var, Value = the value, assign to the project.
CLI:
bws secret create <NAME> "<VALUE>" "$BWS_PROJECT_ID"
# examples:
bws secret create k3s_token "$(openssl rand -base64 64 | tr -d '\n')" "$BWS_PROJECT_ID"
bws secret create TF_VAR_prometheus_grafana_domain "grafana.chrislee.local" "$BWS_PROJECT_ID"
Store literal values. Bitwarden does not expand
${...}. Resolve any interpolation before saving, e.g.grafana.${domain_host}→grafana.chrislee.local,${server_ssh_host}→192.168.1.100.JSON values (
worker_hosts_json,etc_hosts_json,worker_default_taints) are stored raw, with no\"escaping.bws runinjects them natively, which is why the old godotenv escaping rule is gone.Never name a secret after a shell-sensitive variable (
PATH,LD_PRELOAD,LD_LIBRARY_PATH,BASH_ENV,ENV,IFS). These are injected into the shell and can enable code execution. Only the documentedTF_VAR_*,TF_TOKEN_*, and lowercase config names below are expected.
5. Variable reference¶
Every variable from the old .env.sample becomes a secret in the project (name = env var). 🔑 marks sensitive values; the rest is config. Store literal values. Example values use the old template defaults, so substitute your own. Gate = only needed when that module's *_enable flag is true.
Global / Terraform Cloud¶
| Variable | Value / how to obtain | Purpose | |
|---|---|---|---|
TF_TOKEN_app_terraform_io |
🔑 | Terraform Cloud → User Settings → Tokens | Auth to Terraform Cloud remote state; needed before terraform init |
host_machine_architecture |
amd64 (or arm64) |
Build/target architecture | |
domain_host |
chrislee.local |
Base domain all service hostnames derive from |
Stage 0: cloud provisioning¶
Only needed when provisioning cloud machines; stage 0 is a separate root that a LAN-only cluster never runs. The first four have no Terraform default, so running stage 0 without them fails the plan rather than producing an instance nothing can reach. Account setup walks through obtaining every value below.
| Variable | Value / how to obtain | Purpose | |
|---|---|---|---|
TF_VAR_stage0_oci_accounts |
raw JSON object keyed by account label, see below | OCI accounts and their node lists | |
TF_VAR_stage0_oci_private_keys |
🔑 | raw JSON object, same keys, PEM values | OCI API signing keys |
TF_VAR_stage0_ssh_public_key |
cat ~/.ssh/id_rsa.pub |
Injected into every provisioned node for the ubuntu user. Required: an empty value fails the plan |
|
TF_VAR_stage0_tailscale_auth_key |
🔑 | admin console, tagged tag:k8s-node |
Read by cloud-init on every node in the apply. Required: an empty value fails the plan. More than one node at a time needs a reusable key, see Security posture |
TF_VAR_stage0_ssh_ingress_cidrs |
[] |
Sources allowed to reach port 22 from the internet. Empty creates no rule. Entries must be IPv4 CIDRs of /24 or narrower |
|
TF_VAR_stage0_budget_enable |
false |
Meaningless on an unupgraded Always Free account, which cannot be charged |
One key must be exactly account1, because stage0/providers.tf looks it up by that literal. A second account is one more object here keyed account2, plus the two blocks it needs in stage0/, described in the account model. One apply covers every account.
{
"account1": {
"region": "ap-sydney-1",
"tenancy_ocid": "ocid1.tenancy.oc1..aaaaaaaa...",
"user_ocid": "ocid1.user.oc1..aaaaaaaa...",
"fingerprint": "a1:b2:c3:d4:e5:f6:07:18:29:3a:4b:5c:6d:7e:8f:90",
"compartment_ocid": "ocid1.compartment.oc1..aaaaaaaa...",
"nodes": {
"oci-worker-01": { "ocpus": 2, "memory_gbs": 12, "boot_disk_gbs": 100 }
}
}
}
The three shape fields are optional and default to the values shown, so "oci-worker-01": {} is equivalent. Each node key becomes the instance name, the Tailscale hostname and the Kubernetes node name.
The private key is multi-line, so build the JSON rather than pasting it. Run this on the host: the container's $HOME is a bind mount of container/root, so ~/.oci is empty there.
bws secret create TF_VAR_stage0_oci_private_keys "$(python3 -c '
import json, pathlib
print(json.dumps({"account1": pathlib.Path.home().joinpath(".oci/oci_api_key.pem").read_text()}))
')" "$BWS_PROJECT_ID"
TF_VAR_stage0_oci_accounts is deliberately not marked sensitive in Terraform. OCIDs and fingerprints are identifiers rather than secrets, and Terraform rejects for_each derived from a sensitive value, which the nodes map depends on.
Stage 1: cluster / Ansible¶
| Variable | Value / how to obtain | Purpose | |
|---|---|---|---|
kubernetes_cluster_type |
kubeadm (or k3s / minikube) |
Which cluster provisioner to run | |
server_ssh_host |
192.168.1.100 |
Control-plane node IP | |
server_ssh_user |
🔑 | your node SSH username | SSH login user for provisioning |
server_ssh_port |
2222 |
SSH port (must not be 22) | |
worker_hosts_json |
[] or raw JSON array |
Worker node definitions (raw JSON, no escaping) | |
worker_default_taints |
[{"key":"node.homelab/class","value":"low-power","effect":"NoSchedule"}] |
Default taint for workers without their own | |
k3s_extra_server_args |
(empty) | Extra args for the k3s server | |
k3s_token |
🔑 | openssl rand -base64 64 \| tr -d '\n' |
k3s node join secret |
sshd_port |
2222 |
Hardened sshd port (matches server_ssh_port) |
|
wireguard_port |
51820 |
WireGuard listen port | |
docker_default_data_path |
/var/lib/docker |
Docker data root | |
etc_hosts_json |
[] or raw JSON array |
Extra /etc/hosts entries (raw JSON) |
Stage 1: Tailscale node transport (gate: tailscale_node_enable)¶
Separate from the Stage 2 Tailscale block below, and the two keys are not interchangeable: this one must carry tag:k8s-node, the stage 2 one carries tag:k8s-gateway, and the tailnet ACL grants them different access. See tailscale_node for how to generate the key.
| Variable | Value / how to obtain | Purpose | |
|---|---|---|---|
tailscale_node_enable |
false |
Install host-level tailscaled on every cluster node | |
tailscale_auth_key |
🔑 | admin console, tagged tag:k8s-node |
Node join, read only on a node's first run |
hostname_prefix |
homelab |
Prefix for tailnet machine names, no trailing dash |
hostname_prefix is the stage 1 half of a value shared with stage 2's TF_VAR_hostname_prefix. Keep the two in step; they are separate secrets only because stage 1 reads lowercase environment variables and Terraform reads TF_VAR_*. The per-device suffix is not a secret: the control plane's is set in inventory.yml, each worker's in worker_hosts_json, and the gateway's is fixed by the stage 2 module.
Stage 0 has no hostname_prefix. Each node key in stage0_oci_accounts is the tailnet hostname verbatim, so name the keys to match the prefix by hand.
Stage 2: Kubernetes / ingress¶
| Variable | Value / how to obtain | Purpose | |
|---|---|---|---|
TF_VAR_ingress_enable_tls |
true |
Generate TLS certs when true | |
TF_VAR_host_machine_architecture |
amd64 |
Passthrough of host_machine_architecture |
|
TF_VAR_kubernetes_override_ip |
192.168.1.100 |
Cluster external IP override | |
TF_VAR_kubernetes_override_domains |
space-separated domain list (flattened) | Domains routed to the cluster |
Stage 2: Nginx¶
| Variable | Value / how to obtain | Purpose | |
|---|---|---|---|
TF_VAR_nginx_service_loadbalancer_ip |
192.168.1.100 |
LoadBalancer IP for the ingress | |
TF_VAR_nginx_frontend_basic_auth_base64 |
🔑 | htpasswd -nb user password \| openssl base64 |
HTTP basic-auth gate on the frontend |
TF_VAR_nginx_client_max_body_size |
10M |
Max request body size | |
TF_VAR_nginx_client_body_buffer_size |
10M |
Request body buffer size |
Stage 2: Cert Manager¶
| Variable | Value / how to obtain | Purpose | |
|---|---|---|---|
TF_VAR_cert_manager_acme_email |
chris@chrislee.local |
ACME registration email | |
TF_VAR_cert_manager_ingress_class |
nginx |
Ingress class for ACME solver | |
TF_VAR_cert_manager_host_alias_ip |
192.168.1.100 |
Hairpin-NAT host alias IP | |
TF_VAR_cert_manager_host_alias_hostnames |
comma-separated hostnames (flattened) | Hairpin-NAT host aliases |
Stage 2: Longhorn¶
| Variable | Value / how to obtain | Purpose | |
|---|---|---|---|
TF_VAR_longhorn_default_settings_default_data_path |
/var/lib/longhorn |
Longhorn data path | |
TF_VAR_longhorn_ingress_class_name |
nginx |
Ingress class | |
TF_VAR_longhorn_ingress_host |
k8s.chrislee.local |
Longhorn UI host |
Stage 2: MinIO¶
| Variable | Value / how to obtain | Purpose | |
|---|---|---|---|
TF_VAR_minio_tenant_pools_size |
100Gi |
Tenant pool capacity | |
TF_VAR_minio_tenant_ingress_class_name |
nginx |
Ingress class | |
TF_VAR_minio_tenant_ingress_api_host |
minio.chrislee.local |
S3 API host | |
TF_VAR_minio_tenant_ingress_console_host |
minio-console.chrislee.local |
Console host | |
TF_VAR_minio_internal_endpoint |
minio.minio-tenant.svc.cluster.local:80 |
In-cluster S3 endpoint. Must be the minio ClusterIP service on port 80 |
Stage 2: GitLab¶
| Variable | Value / how to obtain | Purpose | |
|---|---|---|---|
TF_VAR_gitlab_global_hosts_domain |
chrislee.local |
GitLab base domain | |
TF_VAR_gitlab_global_hosts_host_suffix |
(empty) | Optional host suffix | |
TF_VAR_gitlab_global_hosts_external_ip |
192.168.1.100 |
External IP | |
TF_VAR_gitlab_global_ingress_class |
nginx |
Ingress class | |
TF_VAR_gitlab_global_ingress_provider |
nginx |
Ingress provider | |
TF_VAR_gitlab_certmanager_issuer_email |
chris@chrislee.local |
Issuer email | |
TF_VAR_gitlab_postgres_storage_size |
20Gi |
CloudNativePG volume | |
TF_VAR_gitlab_valkey_persistence_size |
2Gi |
Valkey volume | |
TF_VAR_gitlab_gitaly_persistence_size |
50Gi |
Gitaly volume. Backs a StatefulSet claim template, which is immutable, so this must match the existing volume | |
TF_VAR_gitlab_toolbox_persistence_size |
20Gi |
Toolbox volume | |
TF_VAR_gitlab_toolbox_backups_cron_persistence_size |
30Gi |
Backup staging volume | |
TF_VAR_gitlab_runner_authentication_token |
🔑 | GitLab → Admin → CI/CD → Runners → New instance runner (allow untagged) | Registers the CI runner |
TF_VAR_gitlab_minio_host |
minio.chrislee.local |
Object storage host | |
TF_VAR_gitlab_minio_endpoint |
https://minio.chrislee.local |
Object storage endpoint | |
TF_VAR_gitlab_minio_use_https |
True |
Use HTTPS to object storage |
Stage 2: Prometheus Stack¶
| Variable | Value / how to obtain | Purpose | |
|---|---|---|---|
TF_VAR_prometheus_alertmanager_domain |
alertmanager.chrislee.local |
Alertmanager host | |
TF_VAR_prometheus_grafana_domain |
grafana.chrislee.local |
Grafana host | |
TF_VAR_prometheus_ingress_class_name |
nginx |
Ingress class | |
TF_VAR_prometheus_prometheus_domain |
prometheus.chrislee.local |
Prometheus host | |
TF_VAR_prometheus_persistence_size |
10Gi |
Prometheus volume | |
TF_VAR_prometheus_alertmanager_slack_channel |
notification |
Slack channel for alerts | |
TF_VAR_prometheus_alertmanager_slack_credentials |
🔑 | api.slack.com/apps → Install App → Bot User OAuth Token | Alertmanager → Slack delivery |
TF_VAR_prometheus_minio_job_bearer_token |
🔑 | mc admin prometheus generate minio |
Scrape MinIO cluster metrics |
TF_VAR_prometheus_minio_job_node_bearer_token |
🔑 | mc admin prometheus generate minio node |
Scrape MinIO node metrics |
TF_VAR_prometheus_minio_job_bucket_bearer_token |
🔑 | mc admin prometheus generate minio bucket |
Scrape MinIO bucket metrics |
TF_VAR_prometheus_minio_job_resource_bearer_token |
🔑 | mc admin prometheus generate minio resource |
Scrape MinIO resource metrics |
The channel name carries no leading #. The values template adds it, so #notification renders as ##notification and every chat.postMessage fails with channel_not_found. terraform plan rejects a leading # rather than applying it.
Stage 2: Logging / Elasticsearch / Kibana¶
| Variable | Value / how to obtain | Purpose | |
|---|---|---|---|
TF_VAR_logging_module_enable |
true |
Enable the logging module | |
TF_VAR_elasticsearch_storage_size |
10Gi |
Elasticsearch volume | |
TF_VAR_kibana_ingress_class_name |
nginx |
Ingress class | |
TF_VAR_kibana_domain |
kibana.chrislee.local |
Kibana host |
Stage 2: Kubecost¶
| Variable | Value / how to obtain | Purpose | |
|---|---|---|---|
TF_VAR_kubecost_ingress_host |
cost.chrislee.local |
Kubecost host | |
TF_VAR_kubecost_ingress_class_name |
nginx |
Ingress class | |
TF_VAR_kubecost_cluster_id |
cluster-one |
Stamped into every ETL record. Changing it on a live install orphans the cost history | |
TF_VAR_kubecost_storage_class_name |
longhorn |
Storage class for the Kubecost volumes |
Stage 2: Tailscale (gate: TF_VAR_tailscale_enable)¶
| Variable | Value / how to obtain | Purpose | |
|---|---|---|---|
TF_VAR_tailscale_enable |
false |
Enable Tailscale | |
TF_VAR_tailscale_auth_key |
🔑 | admin console, tagged tag:k8s-gateway |
Gateway auth to the tailnet |
TF_VAR_tailscale_advertise_routes |
192.86.0.0/24 |
Subnet routes advertised | |
TF_VAR_hostname_prefix |
homelab |
Prefix for tailnet machine names, no trailing dash |
The gateway registers as <prefix>-gateway, so the name is not separately configurable. Keep TF_VAR_hostname_prefix equal to stage 1's hostname_prefix.
The key must be reusable, because the pod re-authenticates on every container start rather than only on first join, and it must be re-issued before it expires or the pod stops starting. See issuing the auth key for the console procedure and the expiry trap.
Stage 2: WireGuard (gate: TF_VAR_wireguard_enable)¶
| Variable | Value / how to obtain | Purpose | |
|---|---|---|---|
TF_VAR_wireguard_enable |
false |
Enable WireGuard | |
TF_VAR_wireguard_ingress_host |
vpn.chrislee.local |
VPN host | |
TF_VAR_wireguard_port |
51820 |
Listen port |
Stage 2: ArgoCD¶
| Variable | Value / how to obtain | Purpose | |
|---|---|---|---|
TF_VAR_argocd_apps_repo_url |
(empty, or repo URL) | Root apps repo for ApplicationSet | |
TF_VAR_argocd_config_repositories_json_encoded |
🔑 | [] |
Repository credentials rendered into configs.repositories; empty in this deployment |
TF_VAR_argocd_domain |
argocd.chrislee.local |
ArgoCD host | |
TF_VAR_argocd_rbac_policy_default |
"" |
Optional fallback role for non-admin identities; empty requires explicit policy grants | |
TF_VAR_argocd_ssh_known_hosts_base64 |
"" |
SSH repository host keys; currently unused | |
TF_VAR_argocd_rbac_policy_csv |
multi-line RBAC CSV | Extra RBAC policy rules |
An exported TF_VAR_argocd_rbac_policy_default overrides the Terraform default of "" and grants every authenticated identity that role. Before applying, confirm that the intended TF_VAR_argocd_rbac_policy_csv grants human access, remove any stored default-role variable, reload without its already-exported value by running unset TF_VAR_argocd_rbac_policy_default; bws-load, and verify that the Terraform plan sets argocd-rbac-cm.data["policy.default"] to an empty string.
Stage 2: ArgoCD Image Updater (gate: TF_VAR_argocd_image_updater_enable)¶
| Variable | Value / how to obtain | Purpose | |
|---|---|---|---|
TF_VAR_argocd_image_updater_enable |
false |
Enable image updater | |
TF_VAR_container_registry_prefix |
registry.chrislee.local |
Registry prefix | |
TF_VAR_container_registry_api_url |
https://registry.chrislee.local |
Registry API URL | |
TF_VAR_container_registry_credentials |
🔑 | GitLab deploy token, read_registry scope, as username:token |
Pull images from the registry |
TF_VAR_argocd_apps_git_username |
argocd-image-updater |
Git user for write-back | |
TF_VAR_argocd_apps_git_password |
🔑 | GitLab project access token, write_repository scope |
Write image-tag bumps to argocd-apps repo |
Stage 2: OAuth2 Proxy + Auth0¶
| Variable | Value / how to obtain | Purpose | |
|---|---|---|---|
TF_VAR_auth_ingress_class_name |
nginx |
Ingress class | |
TF_VAR_auth_oauth2_proxy_host |
auth.chrislee.local |
OAuth2 proxy host | |
TF_VAR_auth_oauth2_proxy_cookie_domains |
[".chrislee.local"] |
Cookie domains | |
TF_VAR_auth_oauth2_proxy_whitelist_domains |
["*.chrislee.local"] |
Redirect whitelist | |
TF_VAR_auth_auth0_domain |
🔑 | Auth0 dashboard → Application settings | Auth0 tenant for SSO |
TF_VAR_auth_auth0_client_id |
🔑 | Auth0 dashboard → Application settings | Auth0 app identifier |
TF_VAR_auth_auth0_client_secret |
🔑 | Auth0 dashboard → Application settings | Auth0 app secret; gates all web services |
Stage 2: Datadog (gate: TF_VAR_datadog_enable)¶
| Variable | Value / how to obtain | Purpose | |
|---|---|---|---|
TF_VAR_datadog_enable |
false |
Enable Datadog | |
TF_VAR_datadog_site |
datadoghq.com |
Datadog site | |
TF_VAR_datadog_cluster_name |
homelab |
Cluster name tag | |
TF_VAR_datadog_api_key |
🔑 | Datadog → Org Settings → API Keys | Agent ingestion |
TF_VAR_datadog_app_key |
🔑 | Datadog → Org Settings → Application Keys | API/app-scoped access |
Stage 2: LiteLLM (gate: TF_VAR_litellm_enable)¶
| Variable | Value / how to obtain | Purpose | |
|---|---|---|---|
TF_VAR_litellm_enable |
false |
Enable LiteLLM | |
TF_VAR_litellm_domain |
litellm.chrislee.local |
Proxy host, serves both /v1 and /ui |
|
TF_VAR_litellm_ingress_class_name |
nginx |
Ingress class | |
TF_VAR_litellm_storage_size |
10Gi |
Postgres volume size | |
TF_VAR_litellm_storage_class_name |
longhorn |
Storage class | |
TF_VAR_litellm_ui_paths |
["/ui","/sso","/litellm-asset-prefix","/fallback/login","/login","/docs","/redoc","/openapi.json","/routes","/config/yaml","/public"] |
Paths routed behind oauth2-proxy. Anything omitted is served unauthenticated | |
TF_VAR_litellm_chart_version |
1.89.2 |
litellm-helm chart pin. Bump together with the image tag |
|
TF_VAR_litellm_image_tag |
1.89.2 |
ghcr.io/berriai/litellm-database pin |
|
TF_VAR_litellm_postgres_image_tag |
18.4-alpine |
Must end in -alpine: the pod sets fs_group = 70 |
|
TF_VAR_litellm_replicas |
1 |
Replica count | |
TF_VAR_litellm_master_key |
🔑 | echo "sk-$(openssl rand -hex 24)" |
Admin and API superuser key |
TF_VAR_litellm_salt_key |
🔑 | echo "sk-$(openssl rand -hex 24)" |
Encrypts DB-stored provider credentials. Write once, never rotate |
TF_VAR_litellm_db_password |
🔑 | openssl rand -hex 16 |
Postgres password, min 16 chars, only A-Z a-z 0-9 _ . ~ -. It is interpolated into a postgresql:// URI, so reserved characters would corrupt the connection string |
TF_VAR_litellm_provider_secrets |
🔑 | JSON object, e.g. {"OPENAI_API_KEY":"sk-…","ANTHROPIC_API_KEY":"sk-ant-…"} |
Provider keys exported to the pod as env vars |
Rotating TF_VAR_litellm_salt_key after models have been added through /ui makes every stored provider credential permanently unreadable. Treat it as write-once.
Stage 2: OmniRoute (gate: TF_VAR_omniroute_enable)¶
| Variable | Value / how to obtain | Purpose | |
|---|---|---|---|
TF_VAR_omniroute_enable |
false |
Enable OmniRoute | |
TF_VAR_omniroute_domain |
omniroute.chrislee.local |
Host serving both the open API prefixes (/v1, /api/v1) and the dashboard |
|
TF_VAR_omniroute_ingress_class_name |
nginx |
Ingress class for both ingresses | |
TF_VAR_omniroute_storage_size |
5Gi |
SQLite volume size | |
TF_VAR_omniroute_storage_class_name |
longhorn |
Storage class | |
TF_VAR_omniroute_chart_version |
0.2.2 |
omniroute chart pin. Bump together with the image tag |
|
TF_VAR_omniroute_image_tag |
"" |
diegosouzapw/omniroute tag. Empty uses the chart appVersion; use -web for web-cookie providers |
|
TF_VAR_omniroute_public_paths |
["/api/v1", "/v1"] |
Paths routed to the open API ingress. Anything omitted is gated by oauth2-proxy | |
TF_VAR_omniroute_gated_admin_suffixes |
["/management", "/agents", "/accounts", "/registered-keys"] |
Admin suffixes pulled back behind oauth2-proxy, applied to every public path prefix | |
TF_VAR_omniroute_initial_password |
🔑 | openssl rand -base64 24 |
First-boot dashboard password, min 12 chars |
TF_VAR_omniroute_jwt_secret |
🔑 | openssl rand -hex 32 |
Signs dashboard sessions, min 32 chars. Rotatable |
TF_VAR_omniroute_api_key_secret |
🔑 | openssl rand -hex 32 |
Encrypts stored provider keys, min 32 chars. Write once, never rotate |
TF_VAR_omniroute_storage_encryption_key |
🔑 | openssl rand -hex 32 |
Encrypts the database at rest, min 32 chars. Write once, never rotate |
The four credential variables populate the omniroute-auth Kubernetes Secret, mounted into the pod with envFrom as the keys JWT_SECRET, API_KEY_SECRET, INITIAL_PASSWORD, and STORAGE_ENCRYPTION_KEY.
Rotating TF_VAR_omniroute_api_key_secret (API_KEY_SECRET) or TF_VAR_omniroute_storage_encryption_key (STORAGE_ENCRYPTION_KEY) after providers have been added makes every stored credential permanently unreadable. Treat both as write-once. TF_VAR_omniroute_jwt_secret only signs sessions and may be rotated (it logs everyone out).
Stage 2: Cloudflare Tunnel (gate: TF_VAR_cloudflare_tunnel_enable)¶
| Variable | Value / how to obtain | Purpose | |
|---|---|---|---|
TF_VAR_cloudflare_tunnel_enable |
false |
Enable the tunnel | |
TF_VAR_cloudflare_tunnel_token |
🔑 | Cloudflare → Zero Trust → Networks → Tunnels → copy --token eyJ… |
cloudflared connector auth |
TF_VAR_cloudflare_tunnel_chart_version |
0.1.2 |
Helm chart version | |
TF_VAR_cloudflare_tunnel_image_tag |
(empty) | cloudflared image tag (empty = chart default) | |
TF_VAR_cloudflare_tunnel_replica_count |
2 |
Replica count |
6. Manage secrets day-to-day¶
set -a; . .env; set +a
bws secret list # list all (names + ids)
bws secret get <secret-id> # read one
bws secret edit <secret-id> --value "<new value>"
bws secret delete <secret-id>
After editing a secret, run bws-load inside the container to pull the change into your current shell (see "Reload without restarting" above).
Rotate the access token: create a new token on the machine account, update .env, then revoke the old one. Least privilege: the machine account only needs can read.
7. Verify¶
task docker:exec # enter the container; .bashrc auto-injects secrets
# inside the container:
echo "$TF_VAR_prometheus_grafana_domain" # -> grafana.chrislee.local (flattened, not ${domain_host})
task stage2:terraform:plan # authenticates via TF_TOKEN_app_terraform_io
task stage1:ansible:ping # reaches the cluster via injected SSH vars
Run secret-consuming tasks inside the container. Host-side terraform is only for no-secret ops (terraform providers lock, init -backend=false, fmt, validate). For CI/scripted runs, call bws run -- <command> directly, since .bashrc only auto-loads for interactive shells.