Skip to main content

Archived wordpress runbook

warning

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

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-cluster in 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-age exists in flux-system and you have your local Age key.
  • DNS: muppit.au already 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-system namespace).
  • 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-coach so 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 wordpress namespace:
    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.au and www records exist and are proxied (orange cloud).
  • If you want www → root redirect, Cloudflare has a ready template (“Redirect from WWW to root”).
    If it warns that www isn’t proxied, remove duplicate DNS entries and let the template create/manage the www CNAME + 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.au
    curl 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.

  1. 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
  2. Login via https://muppit.au/wp-admin with temp-admin.

  3. In Settings → General set Timezone to a Perth‑appropriate zone if prompted (this will be overwritten by your restore anyway).

  4. Verify: create a dummy post and view it; ensure media upload works (writes to /wp-content).

  5. (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; envFrom pulls creds from db-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.


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.

  1. 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.)
  2. Cloudflare Access (Zero Trust) – stronger option:
    • App: https://muppit.au/wp-admin/* and https://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).

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:
  • Confirm:
    • Your historical content is present.
    • /wp-admin is reachable only from your LAN (or WARP/allowed IPs).
    • The public cannot hit /wp-admin or /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-content writable 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 USER or re‑issue in MariaDB as needed.
  • SOPS Age rotation: add new recipient, re‑encrypt, update sops-age Secret, then remove old recipient.
  • Update WP core/plugins: allow HTTPS egress temporarily (NetPol) or do updates via file restore path. Test in a branch/dev overlay first.
  • Backups: add CronJob to dump DB + wp-content to 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/coachwordpress/ai, update: Namespace (wp-ai), host (muppit.ai), Secrets/DB names, and add a new OriginIssuer + Certificate in wp-ai. Configure clusters/my-cluster/wordpress/ai/* with its own GitRepository/Kustomization.