Create the Tenant from Scratch
What You Will Learn
- How to create the tenant directory and namespace YAML
- How to write secrets into HashiCorp Vault using the CLI
- How to create ExternalSecret resources that pull secrets from Vault into Kubernetes
- How each field in an ExternalSecret maps to a Vault path and a Kubernetes Secret key
Step 1: Create the Directory
First, create the tenant directory inside the Flux repo:
# Navigate to the clients directory in the Flux repo
cd ~/workspace/junovy/dds-k8s-cluster/clients
# Create the tenant directory
mkdir -p com.junovy.radical-faeries
This is where all YAML files for this tenant will live. One directory, one tenant.
Step 2: The Namespace (010-namespace.yaml)
Every tenant starts with a namespace. Create 010-namespace.yaml:
# 010-namespace.yaml
# Creates the isolated namespace for the Radical Faeries tenant.
# All their apps, secrets, and networking live inside this namespace.
apiVersion: v1
kind: Namespace
metadata:
name: hst-radical-faeries # hst- prefix = hosting tier
labels:
tier: hosting # Identifies this as a hosting tenant
customer: radical-faeries # Human-readable tenant name
plan: basic # Service tier (free, basic, pro)
managed-by: junovy # Marks this as platform-managed
app.kubernetes.io/part-of: junovy-clients # Groups all client namespaces
ecr-refresh: "enabled" # Auto-refreshes ECR pull credentials
The labels are important. They let you filter and query namespaces later. The ecr-refresh label ensures the namespace gets fresh AWS ECR pull credentials automatically.
Step 3: Store Secrets in Vault
Before Kubernetes can use secrets, they need to exist in Vault. You will run these commands from a terminal with VPN access.
BookStack Secrets
# Store all BookStack secrets under a single Vault path
# Each key=value pair becomes a property you can reference later
vault kv put secret/radical-faeries/bookstack \
app-key="base64:$(openssl rand -base64 32)" \
db-password="$(openssl rand -base64 24)" \
smtp-host="email-smtp.eu-central-1.amazonaws.com" \
smtp-port="587" \
smtp-username="YOUR_SES_SMTP_USERNAME" \
smtp-password="YOUR_SES_SMTP_PASSWORD" \
smtp-from-email="noreply@radical-faeries.junovy.com"
Rallly Secrets
# Generate a secure password for the Rallly database user
RALLLY_DB_PASS="$(openssl rand -base64 24)"
# Store all Rallly secrets under a single Vault path
vault kv put secret/radical-faeries/rallly \
database-url="postgresql://rallly:${RALLLY_DB_PASS}@postgresql.postgresql.svc.cluster.local:5432/rallly_radical_faeries" \
secret-password="$(openssl rand -base64 32)" \
smtp-host="email-smtp.eu-central-1.amazonaws.com" \
smtp-port="587" \
smtp-username="YOUR_SES_SMTP_USERNAME" \
smtp-password="YOUR_SES_SMTP_PASSWORD" \
smtp-from-email="noreply@radical-faeries.junovy.com"
Verify the Secrets
After writing them, confirm they are stored:
# Read back BookStack secrets (shows all keys and values)
vault kv get secret/radical-faeries/bookstack
# Read back Rallly secrets
vault kv get secret/radical-faeries/rallly
You should see all the keys you just stored listed in the output. If a key is missing, run the vault kv put command again -- it overwrites the entire path, so include all keys every time.
Step 4: BookStack ExternalSecret (020-externalsecret-bookstack.yaml)
An ExternalSecret tells the External Secrets Operator: "Go to Vault, read these values, and create a Kubernetes Secret from them." Here is the annotated YAML:
# 020-externalsecret-bookstack.yaml
# Pulls BookStack secrets from HashiCorp Vault and creates a K8s Secret.
# The HelmRelease will reference this Secret for environment variables.
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: bookstack-secrets # Name of this ExternalSecret resource
namespace: hst-radical-faeries # Must match the tenant namespace
spec:
refreshInterval: 1h # Re-sync from Vault every hour
secretStoreRef:
name: vault # References the ClusterSecretStore named "vault"
kind: ClusterSecretStore # Cluster-wide (not namespace-scoped)
target:
name: bookstack-secrets # Name of the K8s Secret that gets created
creationPolicy: Owner # ExternalSecret owns the Secret (deletes with it)
data:
# Each entry maps: Vault path + property → K8s Secret key
#
# How to read this:
# remoteRef.key = the Vault path (after secret/)
# remoteRef.property = the specific key within that path
# secretKey = the key name in the resulting K8s Secret
# Application encryption key (Laravel APP_KEY)
- secretKey: app-key
remoteRef:
key: radical-faeries/bookstack
property: app-key
# MySQL database password
- secretKey: db-password
remoteRef:
key: radical-faeries/bookstack
property: db-password
# SMTP mail configuration
- secretKey: smtp-host
remoteRef:
key: radical-faeries/bookstack
property: smtp-host
- secretKey: smtp-port
remoteRef:
key: radical-faeries/bookstack
property: smtp-port
- secretKey: smtp-username
remoteRef:
key: radical-faeries/bookstack
property: smtp-username
- secretKey: smtp-password
remoteRef:
key: radical-faeries/bookstack
property: smtp-password
- secretKey: smtp-from-email
remoteRef:
key: radical-faeries/bookstack
property: smtp-from-email
How the Mapping Works
Here is the flow for a single secret entry:
Vault ExternalSecret K8s Secret
───── ────────────── ──────────
secret/radical-faeries/bookstack remoteRef: data:
└── db-password: "abc123" key: radical-faeries/bookstack └── db-password: "YWJjMTIz"
property: db-password (base64 encoded)
Vault stores the plaintext value. The ExternalSecret reads it. Kubernetes stores it base64-encoded in a Secret object. Your app sees the plaintext value when it reads the Secret.
Step 5: Rallly ExternalSecret (021-externalsecret-rallly.yaml)
Same pattern, different app:
# 021-externalsecret-rallly.yaml
# Pulls Rallly secrets from HashiCorp Vault and creates a K8s Secret.
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: rallly-secrets
namespace: hst-radical-faeries
spec:
refreshInterval: 1h
secretStoreRef:
name: vault
kind: ClusterSecretStore
target:
name: rallly-secrets
creationPolicy: Owner
data:
# Full PostgreSQL connection string
# Format: postgresql://user:password@host:port/database
- secretKey: database-url
remoteRef:
key: radical-faeries/rallly
property: database-url
# Session encryption key (used by Rallly for secure cookies)
- secretKey: secret-password
remoteRef:
key: radical-faeries/rallly
property: secret-password
# SMTP mail configuration
- secretKey: smtp-host
remoteRef:
key: radical-faeries/rallly
property: smtp-host
- secretKey: smtp-port
remoteRef:
key: radical-faeries/rallly
property: smtp-port
- secretKey: smtp-username
remoteRef:
key: radical-faeries/rallly
property: smtp-username
- secretKey: smtp-password
remoteRef:
key: radical-faeries/rallly
property: smtp-password
- secretKey: smtp-from-email
remoteRef:
key: radical-faeries/rallly
property: smtp-from-email
Notice the structure is identical to the BookStack ExternalSecret. The only differences are the names and the Vault path. This consistency is intentional -- once you learn the pattern, every ExternalSecret looks the same.
Step 6: The Kustomization (kustomization.yaml)
The kustomization file ties everything together. For now, it lists the namespace and secrets. You will add more resources in the next page.
# kustomization.yaml
# Master resource list for the Radical Faeries tenant.
# Flux reads this file to know what to deploy.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: hst-radical-faeries # Applied to all resources in this directory
resources:
- 010-namespace.yaml # Namespace (created first)
- 020-externalsecret-bookstack.yaml # BookStack secrets from Vault
- 021-externalsecret-rallly.yaml # Rallly secrets from Vault
You will add the HelmRelease and Ingress files to this list in the next page.
What You Have So Far
At this point, your directory contains:
com.junovy.radical-faeries/
├── kustomization.yaml
├── 010-namespace.yaml
├── 020-externalsecret-bookstack.yaml
└── 021-externalsecret-rallly.yaml
And in Vault:
secret/radical-faeries/bookstack → app-key, db-password, smtp-*
secret/radical-faeries/rallly → database-url, secret-password, smtp-*
The foundation is in place. The namespace exists, the secrets are stored, and ExternalSecrets know how to pull them into Kubernetes. Next, you will deploy the actual applications.
Key Takeaways
- Start with the namespace (010) -- everything else depends on it
- Store secrets in Vault using
vault kv putwith all keys in one command - ExternalSecrets map Vault paths to Kubernetes Secret keys automatically
- The
remoteRef.keyis the Vault path (without thesecret/prefix) andremoteRef.propertyis the specific key - The kustomization.yaml lists every resource Flux should deploy for this tenant
What Is Next
Next up: Deploy Apps for the Faeries where you will write HelmRelease manifests for BookStack and Rallly and create Ingress resources for public access.