redaction (#1)

Add the redacted source file for demo purposes

Reviewed-on: https://source.michaeldileo.org/michael_dileo/Keybard-Vagabond-Demo/pulls/1
Co-authored-by: Michael DiLeo <michael_dileo@proton.me>
Co-committed-by: Michael DiLeo <michael_dileo@proton.me>
This commit was merged in pull request #1.
This commit is contained in:
2025-12-24 13:40:47 +00:00
committed by michael_dileo
parent 612235d52b
commit 7327d77dcd
333 changed files with 39286 additions and 1 deletions

View File

@@ -0,0 +1,272 @@
# WriteFreely Deployment
WriteFreely is a clean, minimalist publishing platform made for writers. This deployment provides a fully functional WriteFreely instance with persistent storage, SSL certificates, and admin access.
## 🚀 Access Information
- **Blog URL**: `https://blog.keyboardvagabond.com`
- **Admin Username**: `mdileo`
- **Admin Password**: Stored in `writefreely-secret` Kubernetes secret
## 📁 File and Folder Locations
### Inside the Pod
```
/writefreely/ # WriteFreely application directory
├── writefreely # Main binary executable
├── writefreely-docker.sh # Docker entrypoint script
├── static/ # CSS, JS, fonts, images
├── templates/ # HTML templates
├── pages/ # Static pages
└── keys/ # Application encryption keys (symlinked to /data/keys)
/data/ # Persistent volume mount (survives pod restarts)
├── config.ini # Main configuration file (writable)
├── writefreely.db # SQLite database
└── keys/ # Encryption keys directory
├── email.aes256 # Email encryption key
├── cookies.aes256 # Cookie encryption key
├── session.aes256 # Session encryption key
└── csrf.aes256 # CSRF protection key
```
### Kubernetes Resources
```
manifests/applications/write-freely/
├── namespace.yaml # writefreely-system namespace
├── deployment.yaml # Main application deployment
├── service.yaml # ClusterIP service (port 8080)
├── ingress.yaml # NGINX ingress with SSL
├── storage.yaml # PersistentVolumeClaim for data
├── secret.yaml # Admin password (SOPS encrypted)
├── configmap.yaml # Configuration template (unused in current setup)
├── kustomization.yaml # Kustomize resource list
└── README.md # This file
```
## ⚙️ Configuration Management
### Edit config.ini
To edit the WriteFreely configuration file:
```bash
# Get current pod name
POD_NAME=$(kubectl -n writefreely-system get pods -l app=writefreely -o jsonpath='{.items[0].metadata.name}')
# Edit config.ini directly
kubectl -n writefreely-system exec -it $POD_NAME -- vi /data/config.ini
# Or copy out, edit locally, and copy back
kubectl -n writefreely-system cp $POD_NAME:/data/config.ini ./config.ini
# Edit config.ini locally
kubectl -n writefreely-system cp ./config.ini $POD_NAME:/data/config.ini
```
### View current configuration
```bash
POD_NAME=$(kubectl -n writefreely-system get pods -l app=writefreely -o jsonpath='{.items[0].metadata.name}')
kubectl -n writefreely-system exec $POD_NAME -- cat /data/config.ini
```
### Restart after config changes
```bash
kubectl -n writefreely-system rollout restart deployment writefreely
```
## 🔧 Admin Commands
WriteFreely includes several admin commands for user and database management:
### Create additional users
```bash
POD_NAME=$(kubectl -n writefreely-system get pods -l app=writefreely -o jsonpath='{.items[0].metadata.name}')
# Create admin user
kubectl -n writefreely-system exec $POD_NAME -- /writefreely/writefreely -c /data/config.ini user create --admin username:password
# Create regular user (requires existing admin)
kubectl -n writefreely-system exec $POD_NAME -- /writefreely/writefreely -c /data/config.ini user create username:password
```
### Reset user password
```bash
POD_NAME=$(kubectl -n writefreely-system get pods -l app=writefreely -o jsonpath='{.items[0].metadata.name}')
kubectl -n writefreely-system exec -it $POD_NAME -- /writefreely/writefreely -c /data/config.ini user reset-pass username
```
### Database operations
```bash
POD_NAME=$(kubectl -n writefreely-system get pods -l app=writefreely -o jsonpath='{.items[0].metadata.name}')
# Initialize database (if needed)
kubectl -n writefreely-system exec $POD_NAME -- /writefreely/writefreely -c /data/config.ini db init
# Migrate database schema
kubectl -n writefreely-system exec $POD_NAME -- /writefreely/writefreely -c /data/config.ini db migrate
```
## 📊 Monitoring and Logs
### View application logs
```bash
# Live logs
kubectl -n writefreely-system logs -f -l app=writefreely
# Recent logs
kubectl -n writefreely-system logs -l app=writefreely --tail=100
```
### Check pod status
```bash
kubectl -n writefreely-system get pods -l app=writefreely
kubectl -n writefreely-system describe pod -l app=writefreely
```
### Check persistent storage
```bash
POD_NAME=$(kubectl -n writefreely-system get pods -l app=writefreely -o jsonpath='{.items[0].metadata.name}')
# Check data directory contents
kubectl -n writefreely-system exec $POD_NAME -- ls -la /data/
# Check database size
kubectl -n writefreely-system exec $POD_NAME -- du -h /data/writefreely.db
# Check encryption keys
kubectl -n writefreely-system exec $POD_NAME -- ls -la /data/keys/
```
## 🔐 Security
### Password Management
The admin password is stored in a Kubernetes secret:
```bash
# View current password (base64 encoded)
kubectl -n writefreely-system get secret writefreely-secret -o jsonpath='{.data.admin-password}' | base64 -d
# Update password (regenerate secret)
# Edit manifests/applications/write-freely/secret.yaml and apply
```
### SSL Certificates
SSL certificates are automatically managed by cert-manager and Let's Encrypt:
```bash
# Check certificate status
kubectl -n writefreely-system get certificates
kubectl -n writefreely-system describe certificate writefreely-tls
```
## 🔄 Backup and Restore
### Database Backup
```bash
POD_NAME=$(kubectl -n writefreely-system get pods -l app=writefreely -o jsonpath='{.items[0].metadata.name}')
# Backup database
kubectl -n writefreely-system exec $POD_NAME -- cp /data/writefreely.db /data/writefreely-backup-$(date +%Y%m%d).db
# Copy backup locally
kubectl -n writefreely-system cp $POD_NAME:/data/writefreely-backup-$(date +%Y%m%d).db ./writefreely-backup-$(date +%Y%m%d).db
```
### Full Data Backup
The entire `/data` directory is stored in a Longhorn persistent volume with automatic S3 backup to Backblaze B2.
## 🐛 Troubleshooting
### Common Issues
1. **"Unable to load config.ini"**: Ensure config file exists in `/data/config.ini` and is writable
2. **"Username admin is invalid"**: Use non-reserved usernames (avoid "admin", "administrator")
3. **"Read-only file system"**: Config file must be in writable location (`/data/config.ini`)
4. **CSS/JS not loading**: Check ingress configuration and static file serving
### Reset to Clean State
```bash
# Delete pod to force recreation
kubectl -n writefreely-system delete pod -l app=writefreely
# If needed, delete persistent data (WARNING: This will delete all blog content)
# kubectl -n writefreely-system delete pvc writefreely-data
```
### Debug Commands
```bash
POD_NAME=$(kubectl -n writefreely-system get pods -l app=writefreely -o jsonpath='{.items[0].metadata.name}')
# Check environment variables
kubectl -n writefreely-system exec $POD_NAME -- env | grep WRITEFREELY
# Check file permissions
kubectl -n writefreely-system exec $POD_NAME -- ls -la /data/
kubectl -n writefreely-system exec $POD_NAME -- ls -la /writefreely/
# Interactive shell for debugging
kubectl -n writefreely-system exec -it $POD_NAME -- sh
```
## ⚠️ **Critical Configuration Settings**
### Theme Configuration (Required)
**Important**: The `theme` setting must not be empty or CSS/JS files will not load properly.
```ini
[app]
theme = write
```
**Symptoms of missing theme**:
- CSS files return 404 or malformed URLs like `/css/.css`
- Blog appears unstyled
- JavaScript not loading
**Fix**: Edit the config file and set `theme = write`:
```bash
POD_NAME=$(kubectl -n writefreely-system get pods -l app=writefreely -o jsonpath='{.items[0].metadata.name}')
kubectl -n writefreely-system exec -it $POD_NAME -- vi /data/config-writable.ini
# Add or update in the [app] section:
# theme = write
# Restart after changes
kubectl -n writefreely-system rollout restart deployment writefreely
```
## 📝 Configuration Reference
Key configuration sections in `config.ini`:
- **[server]**: Host, port, and TLS settings
- **[database]**: Database connection and file paths
- **[app]**: Site name, description, federation settings
- **[auth]**: User authentication and registration settings
- **[federation]**: ActivityPub and federation configuration
- **[users]**: User creation and management settings
For detailed configuration options, see the [WriteFreely documentation](https://writefreely.org/docs/main/admin/config).
## 🔗 Links
- [WriteFreely Documentation](https://writefreely.org/docs/)
- [WriteFreely Admin Commands](https://writefreely.org/docs/main/admin/commands)
- [WriteFreely GitHub](https://github.com/writefreely/writefreely)

View File

@@ -0,0 +1,138 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: writefreely
namespace: writefreely-application
labels:
app: writefreely
spec:
replicas: 1
selector:
matchLabels:
app: writefreely
template:
metadata:
labels:
app: writefreely
spec:
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
initContainers:
- name: setup-keys-symlink
image: busybox:1.35
command: ['sh', '-c']
args:
- |
# Ensure the keys directory exists in WriteFreely's expected location
mkdir -p /writefreely/keys
# Copy keys from persistent storage to WriteFreely's expected location
if [ -d /data/keys ]; then
cp -r /data/keys/* /writefreely/keys/ 2>/dev/null || echo "No keys found in /data/keys"
fi
echo "Keys setup completed"
volumeMounts:
- name: data
mountPath: /data
- name: writefreely-keys
mountPath: /writefreely/keys
securityContext:
runAsUser: 1000
runAsGroup: 1000
containers:
- name: writefreely
image: jrasanen/writefreely
imagePullPolicy: IfNotPresent
command: ["/writefreely/writefreely"]
args: ["-c", "/data/config.ini"]
securityContext:
runAsUser: 1000
runAsGroup: 1000
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
env:
- name: WRITEFREELY_HOST
value: "https://blog.keyboardvagabond.com"
- name: WRITEFREELY_ADMIN_USER
value: "<ADMIN_USER>"
- name: WRITEFREELY_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: writefreely-secret
key: admin-password
- name: WRITEFREELY_BIND_PORT
value: "8080"
- name: WRITEFREELY_BIND_HOST
value: "0.0.0.0"
- name: WRITEFREELY_SITE_NAME
value: "Keyboard Vagabond Blog"
- name: WRITEFREELY_SITE_DESCRIPTION
value: "Personal blog for the Keyboard Vagabond community"
- name: WRITEFREELY_SINGLE_USER
value: "false"
- name: WRITEFREELY_OPEN_REGISTRATION
value: "false"
- name: WRITEFREELY_FEDERATION
value: "true"
- name: WRITEFREELY_PUBLIC_STATS
value: "true"
- name: WRITEFREELY_MONETIZATION
value: "true"
- name: WRITEFREELY_PRIVATE
value: "false"
- name: WRITEFREELY_LOCAL_TIMELINE
value: "false"
- name: WRITEFREELY_USER_INVITES
value: "user"
- name: WRITEFREELY_DEFAULT_VISIBILITY
value: "public"
- name: WRITEFREELY_MAX_BLOG
value: "4"
- name: WRITEFREELY_MIN_USERNAME_LEN
value: "3"
- name: WRITEFREELY_CHORUS
value: "true"
- name: WRITEFREELY_OPEN_DELETION
value: "true"
- name: WRITEFREELY_DATABASE_DATABASE
value: "sqlite3"
- name: WRITEFREELY_SQLITE_FILENAME
value: "/data/writefreely.db"
ports:
- containerPort: 8080
name: http
livenessProbe:
httpGet:
path: /api/me
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/me
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
volumeMounts:
- name: data
mountPath: /data
- name: writefreely-keys
mountPath: /writefreely/keys
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "1Gi"
cpu: "1000m"
volumes:
- name: data
persistentVolumeClaim:
claimName: writefreely-data
- name: writefreely-keys
emptyDir: {}

View File

@@ -0,0 +1,25 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: writefreely-ingress
namespace: writefreely-application
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/proxy-body-size: "20m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
nginx.ingress.kubernetes.io/client-max-body-size: "20m"
spec:
ingressClassName: nginx
tls: []
rules:
- host: blog.keyboardvagabond.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: writefreely
port:
number: 8080

View File

@@ -0,0 +1,16 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- secret.yaml
- storage.yaml
- deployment.yaml
- service.yaml
- ingress.yaml
# commonLabels removed to avoid immutable selector conflict
# commonLabels:
# app.kubernetes.io/name: writefreely
# app.kubernetes.io/instance: writefreely
# app.kubernetes.io/component: blogging

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: writefreely-application

View File

@@ -0,0 +1,40 @@
apiVersion: v1
kind: Secret
metadata:
name: writefreely-secret
namespace: writefreely-application
stringData:
mailgun_private: ENC[AES256_GCM,data:n0GRJFHnjro6EV3Lqr0lu7arWw+aptZvurtq6T/h6gp7/AOiJYUEEaYCrC/4mL88Mqo=,iv:/aTvDVR+AFeH7wqE78q+hrAUSfvnGjb+8UAbrn8B7uI=,tag:aLuGwn6tYHU410dSac9NqA==,type:str]
oauth_client_id: ENC[AES256_GCM,data:ZEgUtXR/G/DONi2oo1ILd0pQtfnZgdq7QDrUlBIEJSAUV7OndyMlmw==,iv:R+gBg7dHhEAVRfg811kVTSekBxYXyOdwGmNq/QVczDU=,tag:UNw6kGUyStUDmY42NSOJlQ==,type:str]
oauth_client_secret: ENC[AES256_GCM,data:DjDT1fqnFEATZn1ra5tiz14JEb6qpqe1yJH0j/kwO0FRSv3SR8UpXkra7FGgmKSoT5kuJk3xVdgtR8YcaiQg7wd0PYJrew8HZU+yU+b2DvoziBdbDs7z9p7BpOycbXkIvaO3OnaayrvG5+p2NMnH94ESAPYY23kdSVTUTueHWsc=,iv:qv4DyhFSk9qBOCwAC4vtYlZzI3ICjpNy8RDzE0G6gAA=,tag:JgPABIwn/UO1YxDgnC9k7Q==,type:str]
admin-password: ENC[AES256_GCM,data:2sA8Xm+GmhPaxuyKDxX3f99E6IViy/ZLsiyD49NVjnukT5I23c0ESHg9mgWeWE7rW1U=,iv:fm62I2K7Ygo3Y0S5e5vinfh5SKt0ak/gw8g/AiNsl50=,tag:/BFo8wyuhO/ehZBmupeDKA==,type:str]
sops:
lastmodified: "2025-11-22T17:36:42Z"
mac: ENC[AES256_GCM,data:Il/P5j0eqjpJS8fBv1O/dbGAq3F07i87iDgo4YF0ONuEBNExAnJC65yVasqdlHBBq68MuUHfBjIw1TPYQlChWgQRHCwnOE6pj8SCotc2JV1BUWA1eqDRyfEUbhBihXiBphzbGxnZouBWkZaKUcC+Yl7YV3JXUoO0uqs0KJei/WU=,iv:G9sMHMYQbAvDniHVDP3o/g9DCfvQfF2rp7MXYMYhksc=,tag:nnN+1friVSe2ebQqPk59cQ==,type:str]
pgp:
- created_at: "2025-07-21T23:09:33Z"
enc: |-
-----BEGIN PGP MESSAGE-----
hF4DZT3mpHTS/JgSAQdAFldU/sqJt/BmezG6ObjSm/vMgdjSZoeD2TEvjvY7Kigw
tbKB7OCUP8c5tjzbv+kbrt5XMKVHu3neeWLGpGipoxLFYW7hJbbg2t5gIvT0Cdtu
1GYBCQIQr17eIY+J4ciBhF3KkXV2vdIN4VHaHEHnZumv9tpF/tjHXxT7dpQp3zT0
4mcoNlDRv4b6OFVR+33wELBzv14MoRSp5DyKZgAcJ4iZ3sdiSw/BxskGW6OI/ChY
ZY4efT3JRf4=
=KiFJ
-----END PGP MESSAGE-----
fp: B120595CA9A643B051731B32E67FF350227BA4E8
- created_at: "2025-07-21T23:09:33Z"
enc: |-
-----BEGIN PGP MESSAGE-----
hF4DSXzd60P2RKISAQdAO4VYmbDjZr+C2tLwc5F9he6B0bpR+vQ1DyxetqFuCWAw
re7OYzngq7yg7XFBlkrPmxDtkSDnGseEiTlba294njGuCUwXhAaQ+u2sJoIewTYB
1GYBCQIQ0/ELW/O7iTrrksGaG5VRYSnKfZbsU++Gm5AZRPVJaVqLScf8o2bUjlY5
Vfc8aeMPNSbjOhMm5DcWt/AjLd1o6QldXrCMoCL/hU8Eou6gTXTpOPSqbMnmWWqM
8bUNgZvv7PI=
=I3tS
-----END PGP MESSAGE-----
fp: 4A8AADB4EBAB9AF88EF7062373CECE06CC80D40C
encrypted_regex: ^(data|stringData)$
version: 3.10.2

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: writefreely
namespace: writefreely-application
spec:
type: ClusterIP
ports:
- port: 8080
targetPort: 8080
protocol: TCP
name: http
selector:
app: writefreely

View File

@@ -0,0 +1,18 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: writefreely-data
namespace: writefreely-application
labels:
# Enable S3 backup for WriteFreely data (daily + weekly)
recurring-job.longhorn.io/source: "enabled"
recurring-job-group.longhorn.io/longhorn-s3-backup: "enabled"
recurring-job-group.longhorn.io/longhorn-s3-backup-weekly: "enabled"
spec:
accessModes:
- ReadWriteMany
storageClassName: longhorn-retain
volumeName: writefreely-data-recovered-pv
resources:
requests:
storage: 2Gi