Archived wordpress runbook
Archived on: 6 December 2025. Risk: high.
Superseded by: /kubernetes/provisioning.
Notes: Replaced by detailed WordPress series. Keep for lab reference only.
WordPress GitOps via Flux
This runbook provides end-to-end instructions on how to deploy one or more wordpress sites onto the cluster using GitOps automation via Flux.
- This playbook has been tested on:
- Kubernetes version:
v1.31.9 - OS-Image:
Ubuntu 24.04.2 LTS - Kernel version:
6.8.0-62-generic - Container runtime:
containerd://2.0.5
- Kubernetes version:
1.0.0 - Prerequisites
- Cloudflare runbook completed:
- See Cloudflare runbook.
- Cluster: Kubernetes (ingress-nginx + cert-manager installed and healthy).
- Flux: already bootstrapped, root at
./clusters/my-clusterin flux-config. - GitLab: group and repos available (e.g.,
fluxgitops/flux-config,wordpress/coach). - Cloudflare stack: your Origin CA Issuer controller and tunnel are in place (per Cloudflare runbook).
- SOPS/age: cluster secret
sops-ageexists influx-systemand you have your local Age key.
- DNS:
muppit.aualready proxied via Cloudflare to your ingress.
1.0.1 Local tooling (Mac)
No additional tooling required. All tools installed during the Cloudflare runbook.
1.0.2 Environment assumptions
- Flux v2 present (
flux-systemnamespace). - Ingress is ingress-nginx; cert-manager is installed.
- Cloudflare tunnel terminates on your ingress-nginx and verifies origin TLS using Origin CA.
- You have a valid Cloudflare API token to issue Origin CA certs.
1.0.3 Verify
printf "\n== Versions ==\n"
for t in git kubectl kustomize flux sops age jq yq crane; do
printf "%-10s %s\n" "$t" "$($t --version 2>/dev/null | head -n1 || echo 'not found')"
done
1.1 - Wordpress coach website
App repo:
website/coach.git.
1.1.1 Prepare the App Repo in GitLab
On your Mac.
1.1.1.1 Create the GitLab project
- Group:
website - Project:
website/coach - SSH URL (example):
ssh://git-ssh.reids.net.au/website/coach.git
(scp style also works:git@git-ssh.reids.net.au:website/coach.git)
1.1.1.2 Give Flux read access (Deploy Key)
- In the new GitLab project → Settings → Repository → Deploy Keys
- Enable the same Flux deploy key you already use (
flux-ssh-auth), or add its public key again. - If you must recover the public key from the cluster (prefer the original copy instead):
kubectl -n flux-system get secret flux-ssh-auth \
-o jsonpath='{.data.identity}' | base64 -d > /tmp/flux_id
ssh-keygen -y -f /tmp/flux_id > /tmp/flux_id.pub
# paste /tmp/flux_id.pub into Deploy Keys (Read-only)
1.1.2 Clone the app repo & create repo structure
> What directories are we creating?
mkdir -p ~/Projects/website && cd "$_"
git clone https://gitlab.reids.net.au/website/coach.git
cd coach
mkdir -p k8s/prod
Initial repo layout on your Mac (should be empty):
tree -a -I '.git|.DS_Store'
1.1.3 - Repos and paths
Overview.
Top‑level layout (adds wordpress/ for per‑website app repos):
ai/apps/
muppit-apps/
cloudflare/
coach-app/
docs/
fitness-app/
private-docs/
fluxgitops/
flux-config/
wordpress/
coach/
# ai/ (future)
# website3/ (future)
1.1.4 - App repo: wordpress/coach
End state wordpress/coach repo structure:
wordpress/coach/
├── .sops.yaml
└── k8s/
└── prod/
├── 00-namespace.yaml
├── 10-secret-db.enc.yaml
├── 11-secret-wp-salts.enc.yaml
├── 12-secret-cfapi-token.enc.yaml
├── 20-configmap-mariadb.cnf.yaml
├── 21-originissuer.yaml
├── 30-statefulset-mariadb.yaml
├── 31-service-mariadb.yaml
├── 40-pvc-wp-content.yaml
├── 50-deployment-wordpress.yaml
├── 51-service-wordpress.yaml
├── 55-certificate-muppit.yaml
├── 60-ingress.yaml
├── 70-networkpolicies.yaml
└── kustomization.yaml
1.1.5 SOPS policy
Which SOPS yaml is the right one to use?
Add a SOPS policy at repo root:
# .sops.yaml
creation_rules:
- path_regex: k8s/.*\.enc\.ya?ml$
encrypted_regex: '^(data|stringData)$'
age: ['AGE_PUBLIC_KEY_HERE']
Replace AGE_PUBLIC_KEY_HERE with your public key (the one starting with age1...).
OR
# .sops.yaml
creation_rules:
- path_regex: ".*(enc\.ya?ml|secret.*\.ya?ml)$"
encrypted_regex: '^(data|stringData)$'
age: ["age1replacewithyourkeyfromcloudflarerepo"]
1.1.6 Create manifests (plaintext) (DB + WP + TLS)
Create the files under
k8s/prod/.
1.1.6.1 Namespace
# k8s/prod/00-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: wp-coach
labels:
app.kubernetes.io/name: coach
app.kubernetes.io/part-of: wordpress
1.1.6.2 Secret
# k8s/prod/10-secret-db.enc.yaml
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: wp-coach
stringData:
mariadb-root-password: "REPLACE_ME"
mariadb-user: "andy"
mariadb-password: "REPLACE_ME"
mariadb-database: "muppitco"
1.1.6.3 Secret salts
# k8s/prod/11-secret-wp-salts.enc.yaml
apiVersion: v1
kind: Secret
metadata:
name: wp-salts
namespace: wp-coach
stringData:
WORDPRESS_AUTH_KEY: "REPLACE_ME"
WORDPRESS_SECURE_AUTH_KEY: "REPLACE_ME"
WORDPRESS_LOGGED_IN_KEY: "REPLACE_ME"
WORDPRESS_NONCE_KEY: "REPLACE_ME"
WORDPRESS_AUTH_SALT: "REPLACE_ME"
WORDPRESS_SECURE_AUTH_SALT: "REPLACE_ME"
WORDPRESS_LOGGED_IN_SALT: "REPLACE_ME"
WORDPRESS_NONCE_SALT: "REPLACE_ME"
1.1.6.4 Secret CFAPI token
Reuse the same Cloudflare API token value as your cloudflare app repo; this copy must live in
wp-coachso OriginIssuer can issue a cert in this namespace.
# k8s/prod/12-secret-cfapi-token.enc.yaml
apiVersion: v1
kind: Secret
metadata:
name: cfapi-token
namespace: wp-coach
type: Opaque
stringData:
key: PASTE_YOUR_API_TOKEN
1.1.6.5 Maria database config map
# k8s/prod/20-configmap-mariadb.cnf.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mariadb-extra-cnf
namespace: wp-coach
data:
extra.cnf: |
[mysqld]
max_allowed_packet=64M
max_connections=200
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_520_ci
skip-character-set-client-handshake
1.1.6.6 Origin issuer
# k8s/prod/21-originissuer.yaml
apiVersion: cert-manager.k8s.cloudflare.com/v1
kind: OriginIssuer
metadata:
name: cf-origin
namespace: wp-coach
spec:
requestType: OriginECC
auth:
tokenRef:
name: cfapi-token
key: key
1.1.6.7 Maria database stateful set
# k8s/prod/30-statefulset-mariadb.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mariadb
namespace: wp-coach
spec:
serviceName: mariadb
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: mariadb
template:
metadata:
labels:
app.kubernetes.io/name: mariadb
spec:
containers:
- name: mariadb
image: mariadb:10.5.29
env:
- name: MARIADB_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: mariadb-root-password
- name: MARIADB_DATABASE
valueFrom:
secretKeyRef:
name: db-credentials
key: mariadb-database
- name: MARIADB_USER
valueFrom:
secretKeyRef:
name: db-credentials
key: mariadb-user
- name: MARIADB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: mariadb-password
ports:
- name: mysql
containerPort: 3306
volumeMounts:
- name: data
mountPath: /var/lib/mysql
- name: extra-cnf
mountPath: /etc/mysql/conf.d/extra.cnf
subPath: extra.cnf
readinessProbe:
exec:
command: ["/bin/sh", "-c", "mysqladmin ping -uroot -p$MARIADB_ROOT_PASSWORD"]
initialDelaySeconds: 10
periodSeconds: 5
volumes:
- name: extra-cnf
configMap:
name: mariadb-extra-cnf
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 20Gi
1.1.6.8 Maria database service
# k8s/prod/31-service-mariadb.yaml
apiVersion: v1
kind: Service
metadata:
name: mariadb
namespace: wp-coach
spec:
selector:
app.kubernetes.io/name: mariadb
ports:
- name: mysql
port: 3306
targetPort: 3306
1.1.6.9 WP content PVC
# k8s/prod/40-pvc-wp-content.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: wp-content
namespace: wp-coach
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
1.1.6.10 WP deployment
# k8s/prod/50-deployment-wordpress.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: wordpress
namespace: wp-coach
spec:
replicas: 2
selector:
matchLabels:
app.kubernetes.io/name: wordpress
template:
metadata:
labels:
app.kubernetes.io/name: wordpress
spec:
containers:
- name: wordpress
image: wordpress:6.8.3-php8.3-apache
env:
- name: WORDPRESS_DB_HOST
value: "mariadb.wp-coach.svc.cluster.local:3306"
- name: WORDPRESS_DB_USER
valueFrom:
secretKeyRef:
name: db-credentials
key: mariadb-user
- name: WORDPRESS_DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: mariadb-password
- name: WORDPRESS_DB_NAME
valueFrom:
secretKeyRef:
name: db-credentials
key: mariadb-database
- name: WORDPRESS_CONFIG_EXTRA
value: |
define('DB_CHARSET','utf8mb4');
define('DB_COLLATE','utf8mb4_unicode_520_ci');
$table_prefix = 'wp_';
envFrom:
- secretRef:
name: wp-salts
ports:
- name: http
containerPort: 80
volumeMounts:
- name: wp-content
mountPath: /var/www/html/wp-content
readinessProbe:
httpGet:
path: /wp-login.php
port: 80
initialDelaySeconds: 10
periodSeconds: 5
volumes:
- name: wp-content
persistentVolumeClaim:
claimName: wp-content
1.1.6.11 WP service
# k8s/prod/51-service-wordpress.yaml
apiVersion: v1
kind: Service
metadata:
name: wordpress
namespace: wp-coach
spec:
selector:
app.kubernetes.io/name: wordpress
ports:
- name: http
port: 80
targetPort: 80
1.1.6.12 Certificate issuer
Issue an Origin CA certificate in the same namespace as your Ingress (
wp-coach).
# k8s/prod/55-certificate-muppit.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: muppit-au-origin
namespace: wp-coach
spec:
secretName: muppit-au-tls
dnsNames:
- muppit.au
issuerRef:
group: cert-manager.k8s.cloudflare.com
kind: OriginIssuer
name: cf-origin
1.1.6.13 WP ingress
# k8s/prod/60-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: wp-coach
namespace: wp-coach
annotations:
kubernetes.io/ingress.class: nginx
spec:
tls:
- hosts:
- muppit.au
secretName: muppit-au-tls
rules:
- host: muppit.au
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: wordpress
port:
number: 80
1.1.6.14 WP network policies
# k8s/prod/70-networkpolicies.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: wp-coach
spec:
podSelector: {}
policyTypes: ["Ingress","Egress"]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-nginx-to-wordpress
namespace: wp-coach
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: wordpress
policyTypes: ["Ingress"]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- protocol: TCP
port: 80
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-wordpress-to-db-and-dns
namespace: wp-coach
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: wordpress
policyTypes: ["Egress"]
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: wp-coach
podSelector:
matchLabels:
app.kubernetes.io/name: mariadb
ports:
- protocol: TCP
port: 3306
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-db-from-wordpress
namespace: wp-coach
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: mariadb
policyTypes: ["Ingress"]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: wp-coach
podSelector:
matchLabels:
app.kubernetes.io/name: wordpress
ports:
- protocol: TCP
port: 3306
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-wordpress-web-egress
namespace: wp-coach
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: wordpress
policyTypes: ["Egress"]
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
ports:
- protocol: TCP
port: 443
1.1.6.14 Kustomization
# k8s/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: wp-coach
resources:
- 00-namespace.yaml
- 10-secret-db.enc.yaml
- 11-secret-wp-salts.enc.yaml
- 12-secret-cfapi-token.enc.yaml
- 20-configmap-mariadb.cnf.yaml
- 21-originissuer.yaml
- 30-statefulset-mariadb.yaml
- 31-service-mariadb.yaml
- 40-pvc-wp-content.yaml
- 50-deployment-wordpress.yaml
- 51-service-wordpress.yaml
- 55-certificate-muppit.yaml
- 60-ingress.yaml
- 70-networkpolicies.yaml
1.1.7 SOPS encryption
Encrypt the two Secrets and the new token Secret:
sops -e -i k8s/prod/10-secret-db.enc.yaml
sops -e -i k8s/prod/11-secret-wp-salts.enc.yaml
sops -e -i k8s/prod/12-secret-cfapi-token.enc.yaml
Verify they’re encrypted:
head -n 20 k8s/prod/10-secret-db.enc.yaml
head -n 20 k8s/prod/11-secret-wp-salts.enc.yaml
head -n 20 k8s/prod/12-secret-cfapi-token.enc.yaml
grep -n 'sops:' k8s/prod/*.enc.yaml
1.1.8 Git commit and push website\coach
git add .
git commit -m "init coach website"
git push
1.1.9 Confirm repo layout
Final repo layout:
tree -a -I '.git|.DS_Store'
1.2 - Add wordpress/coach to infra repo: flux-config
In the coach GitLab project → Settings → Repository → Deploy Keys → Privately accessible deploy keys
- Enable the same Flux deploy key you already use (
flux-ssh-auth). - When enabled it will appear under Enabled deploy keys.
On the Mac, navigate to the previously cloned flux-config project and pull any updates to ensure it is up to date:
cd ~/Projects/flux-config
git pull
1.2.1 Create manifests (plaintext)
Create the following files under
clusters/my-cluster/wordpress/.
Create a wordpress aggregator in the coach app repo.
fluxgitops/flux-config/
└── clusters/
└── my-cluster/
├── wordpress/
│ ├── kustomization.yaml
│ ├── 00-kustomization-coach.yaml
│ └── coach/
│ ├── source.yaml # GitRepository → wordpress/coach.git
│ └── kustomization.yaml # Kustomization → ./k8s/prod
└── kustomization.yaml # include ./wordpress here
1.2.1.1 Kustomization
# clusters/my-cluster/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./apps
- ./cloudflare
- ./origin-ca-issuer
- ./wordpress
1.2.1.2 Wordpress kustomization
# clusters/my-cluster/wordpress/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- 00-kustomization-coach.yaml
1.2.1.2 Root level kustomization
# clusters/my-cluster/wordpress/00-kustomization-coach.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./coach/source.yaml
- ./coach/kustomization.yaml
1.2.1.3 Coach source
# clusters/my-cluster/wordpress/coach/source.yaml
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: wordpress-coach
namespace: flux-system
spec:
interval: 1m
url: ssh://git@gitlab.reids.net.au/wordpress/coach.git
ref:
branch: main
secretRef:
name: flux-git-deploy-key
1.2.1.4 Coach kustomization
# clusters/my-cluster/wordpress/coach/kustomization.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: wordpress-coach
namespace: flux-system
spec:
interval: 1m
path: ./k8s/prod
prune: true
sourceRef:
kind: GitRepository
name: wordpress-coach
wait: true
timeout: 5m
1.2.2 Git commit and push fluxgitops/flux-config
Need to add top level as well the kustomization file
git add clusters/my-cluster/wordpress
git commit -m "update flux config with coach wordpress website"
git push
1.2.3 Confirm repo layout
Final repo layout:
tree -a -I '.git|.DS_Store'
1.2.4 Reconcile and check
- Force reconciliation instead of waiting for the timer:
flux reconcile source git flux-system -n flux-system \
&& flux reconcile kustomization flux-system -n flux-system --with-source - Check that all sources are applied and ready:
flux get sources git -n flux-system - Check that all Kustomizations are applied and ready
flux get kustomizations -n flux-system
1.2.5 Wait for deployment to be ready
When WordPress and MariaDB pods are Ready and muppit-au-tls exists, proceed.
- Watch app namespace:
kubectl -n wp-coach get pods,svc,ingress - Check Certificate readiness (Origin CA in ns):
kubectl -n wp-coach get certificate muppit-au-origin
kubectl -n wp-coach describe certificate muppit-au-origin | sed -n '1,120p' - Confirm Ingress has TLS secret:
kubectl -n wp-coach get secret muppit-au-tls - Check the
wordpressnamespace:kubectl get all -n wp-coach - Check the config map:
kubectl -n wp-coach get cm - Check the certificates
kubectl -n wp-coach get certificaterequests,certificate
1.2.6 Cloudflare DNS & Redirects
- Ensure
muppit.auandwwwrecords exist and are proxied (orange cloud). - If you want
www → rootredirect, Cloudflare has a ready template (“Redirect from WWW to root”).
If it warns thatwwwisn’t proxied, remove duplicate DNS entries and let the template create/manage thewwwCNAME + rule together.
1.2.7 Check default wordpress
Confirm it is being served over the cloudflare tunnel.
- Check from a workstation connected to the Internet, not LAN. Confirm the hello world test app page has been replaced by the default wordpress page:
curl -I https://muppit.aucurl https://muppit.au - Browse to https://muppit.au → you should see WordPress setup/“Welcome” pages (fresh DB).
- DNS and TLS should be valid (Cloudflare proxied, Origin CA to cluster).
- If site doesn’t load, check:
kubectl -n wp-coach get ingress wp-coach -o wide
kubectl -n wp-coach describe ingress wp-coach
kubectl -n wp-coach logs deploy/wordpress --tail=100
1.3 - Pre‑restore default WordPress test
Ccreate temp admin & login. Goal: verify DB connectivity and app health before restoring your backup.
-
Open https://muppit.au/wp-admin/install.php and complete the install wizard:
- Site Title:
muppit.au (temp) - Admin Username:
temp-admin - Password: (strong, record temporarily)
- Email: your address
- Site Title:
-
Login via https://muppit.au/wp-admin with
temp-admin. -
In Settings → General set Timezone to a Perth‑appropriate zone if prompted (this will be overwritten by your restore anyway).
-
Verify: create a dummy post and view it; ensure media upload works (writes to
/wp-content). -
(Optional) Add Health Check & Troubleshooting plugin to confirm PHP/DB extensions are fine.
Cleanup later: The DB restore will overwrite these temp credentials; no manual removal needed.
1.4 - Restore plan (DB then content)
Keep site up during restores; it will flip to your content once complete.
Execute from workstation where flux is installed and the website backups are stored not from the master node.
- DB restore — Temporary Pod + PVC;
envFrompulls creds fromdb-credentials:# restore.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: restore-tmp
namespace: wp-coach
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 2Gi
---
apiVersion: v1
kind: Pod
metadata:
name: db-restore
namespace: wp-coach
spec:
restartPolicy: Never
containers:
- name: mysql
image: mariadb:10.5.29
command: ["bash","-lc","sleep infinity"]
envFrom:
- secretRef:
name: db-credentials
volumeMounts:
- name: restore
mountPath: /restore
volumes:
- name: restore
persistentVolumeClaim:
claimName: restore-tmp - Apply + import:
kubectl -n wp-coach apply -f restore.yaml
kubectl -n wp-coach cp ./muppitco.sql db-restore:/restore/muppitco.sql
kubectl -n wp-coach exec -it db-restore -- bash -lc 'mysql -hmariadb -u"$MARIADB_USER" -p"$MARIADB_PASSWORD" "$MARIADB_DATABASE" < /restore/muppitco.sql'- Alternative database root import:
kubectl -n wp-coach exec -it statefulset/mariadb -- bash -lc 'mysql -uroot -p"$MARIADB_ROOT_PASSWORD" muppitco < /tmp/muppitco.sql' - Content restore (
wp-content):tar -C /path/to/backup-root -czf wp-content.tar.gz wp-content
kubectl -n wp-coach cp ./wp-content.tar.gz deploy/wordpress:/tmp/wp-content.tar.gz
kubectl -n wp-coach exec deploy/wordpress -- bash -lc 'rm -rf /var/www/html/wp-content/* && tar -xzf /tmp/wp-content.tar.gz -C /var/www/html/ && chown -R www-data:www-data /var/www/html/wp-content' - Change DB user to andy - update Secret + grants):
CREATE USER 'andy'@'%' IDENTIFIED BY 'REPLACE_ME';
GRANT ALL PRIVILEGES ON muppitco.* TO 'andy'@'%';
FLUSH PRIVILEGES; - WordPress uses mysqli (mysqlnd); exact mysqlnd version doesn’t need to match the old host.
1.5 - Reconcile & verify
- Force reconciliation instead of waiting for the timer:
flux reconcile source git flux-system -n flux-system \
&& flux reconcile kustomization flux-system -n flux-system --with-source - Check that all sources are applied and ready:
flux get sources git -n flux-system - Check that all Kustomizations are applied and ready:
flux get kustomizations -n flux-system - Check app namespace health:
kubectl -n wp-coach get all
kubectl -n wp-coach get certificate,secret | grep muppit-au
kubectl -n wp-coach describe certificate muppit-au-origin
kubectl -n wp-coach get ingress wp-coach -o yaml | yq '.spec.tls'
1.6 Re-verify
Post-restore, before locking down wp-admin access.
- Check from a workstation connected to the Internet, not LAN. Confirm the default wordpress app page has been replaced by the transferred content: https://muppit.au:
curl -I https://muppit.aucurl https://muppit.au - Site checks (from LAN or Internet):
- open https://muppit.au/wp-admin
- open https://muppit.au
- open https://www.muppit.au
- Confirm:
- Your historical content is present.
/wp-adminis reachable.
1.7 Delete temporary restore pods
kubectl -n wp-coach delete pod db-restore && kubectl -n wp-coach delete pvc restore-tmp
1.8 Lockdown admin access (LAN‑only)
Target: wp-admin/wp-login.php manageable only from WAN IP. database manageable only from LAN 192.168.30.0/24 (no external access).
1.8.1 Website admin
Preferred: Cloudflare Access/WAF.
- Cloudflare WAF rule (dash → Security → WAF → Custom Rules):
- Expression:
(http.request.uri.path eq "/wp-login.php" or http.request.uri.path starts_with "/wp-admin") and not ip.src in { YOUR_PUBLIC_IP }
- Action:
- Block.
- Add exceptions for your office/home public IP(s). (Private RFC1918 like 192.168.30.0/24 are not visible on the public internet; use your WAN IPs here.)
- Expression:
- Cloudflare Access (Zero Trust) – stronger option:
- App:
https://muppit.au/wp-admin/*andhttps://muppit.au/wp-login.php - Policy: Require device with WARP connected and (optionally) specific user/group or device posture checks.
- This effectively limits admin to your LAN devices running WARP (even if your public IP changes).
- App:
You can use both: Access to require identity+device, and WAF to hard block non‑trusted IPs.
1.8.2 Ingress‑level hardening
Optional defense‑in‑depth.
Add rate limiting and XML-RPC/REST restrictions (optional) to the Ingress:
metadata:
annotations:
nginx.ingress.kubernetes.io/limit-rps: "10"
nginx.ingress.kubernetes.io/limit-burst-multiplier: "5"
nginx.ingress.kubernetes.io/server-snippet: |
location = /xmlrpc.php { return 444; }
IP allowlisting inside NGINX won’t see private IP range over the public web; rely on Cloudflare (WAF/Access) for LAN‑only enforcement.
1.8.3 Database management (LAN‑only)
- Do not expose MariaDB externally. It remains ClusterIP only.
- From a LAN workstation: use kubectl port-forward for admin tasks (MySQL client/Workbench):
# Ensure your kubeconfig is only accessible on LAN
kubectl -n wp-coach port-forward statefulset/mariadb 3306:3306
# connect locally:
mysql -h 127.0.0.1 -u andy -p muppitco - NetworkPolicies already limit DB ingress to WordPress. If you temporarily run a db-admin Pod for imports, keep it short‑lived.
1.8.4 Re‑verify
Post‑restore + lockdown.
- App health:
kubectl -n wp-coach get deploy,po,svc,ingress,certificate
kubectl -n wp-coach logs deploy/wordpress --tail=100 - Site checks:
- open https://muppit.au/wp-admin # from LAN or WARP device
- open https://muppit.au # public should not reach /wp-admin
- Confirm:
- Your historical content is present.
/wp-adminis reachable only from your LAN (or WARP/allowed IPs).- The public cannot hit
/wp-adminor/wp-login.php.
1.9 Security & hardening
- Secrets via SOPS (no plaintext in Git).
- Default‑deny NetworkPolicy; allow only nginx → WP, WP → DB + DNS.
- Pin images to exact tags (optional: pin to digests with
crane digest). - Consider
readOnlyRootFilesystem(keep/wp-contentwritable or relocate Apache tmp/logs).
securityContext:
runAsNonRoot: true
runAsUser: 33
runAsGroup: 33
readOnlyRootFilesystem: false
1.10 Operations
- Rotate WP DB password: update Secret → Flux rolls Deploy; apply
ALTER USERor re‑issue in MariaDB as needed. - SOPS Age rotation: add new recipient, re‑encrypt, update
sops-ageSecret, then remove old recipient. - Update WP core/plugins: allow HTTPS egress temporarily (NetPol) or do updates via file restore path. Test in a branch/
devoverlay first. - Backups: add CronJob to dump DB +
wp-contentto S3/NAS. - Version bumps: change container tags in Git, push, watch Flux roll.
1.11 Cleanup / rollback
Emergency remove the WordPress site (keeps cluster controllers intact):
# Remove Kustomization and source for wordpress-coach
kubectl -n flux-system delete kustomization wordpress-coach --ignore-not-found
kubectl -n flux-system delete gitrepository wordpress-coach --ignore-not-found
# Delete the app namespace (removes Deployments/Services/Ingress/Secrets/PVCs)
kubectl delete ns wp-coach --ignore-not-found
# Optional: check for leftover PVs and clean if desired
kubectl get pv | grep wp-coach || true
1.12 “Clone this” for other websites
- Copy
wordpress/coach→wordpress/ai, update: Namespace (wp-ai), host (muppit.ai), Secrets/DB names, and add a new OriginIssuer + Certificate inwp-ai. Configureclusters/my-cluster/wordpress/ai/*with its own GitRepository/Kustomization.