Add K3s dev deployment setup for single-node VPS
Mirrors the prod deploy-k3s/ setup but runs all services in-cluster on a single node: PostgreSQL (replaces Neon), MinIO S3-compatible storage (replaces B2), Redis, API, worker, and admin. Includes fully automated setup scripts (00-init through 04-verify), server hardening (SSH, fail2ban, ufw), Let's Encrypt TLS via Traefik, network policies, RBAC, and security contexts matching prod. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
13
deploy-k3s-dev/.gitignore
vendored
Normal file
13
deploy-k3s-dev/.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Single config file (contains tokens and credentials)
|
||||
config.yaml
|
||||
|
||||
# Generated files
|
||||
kubeconfig
|
||||
|
||||
# Secret files
|
||||
secrets/*.txt
|
||||
secrets/*.p8
|
||||
secrets/*.pem
|
||||
secrets/*.key
|
||||
secrets/*.crt
|
||||
!secrets/README.md
|
||||
78
deploy-k3s-dev/README.md
Normal file
78
deploy-k3s-dev/README.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# honeyDue — K3s Dev Deployment
|
||||
|
||||
Single-node K3s dev environment that replicates the production setup with all services running locally.
|
||||
|
||||
**Architecture**: 1-node K3s, in-cluster PostgreSQL + Redis + MinIO (S3-compatible), Let's Encrypt TLS.
|
||||
|
||||
**Domains**: `devapi.myhoneydue.com`, `devadmin.myhoneydue.com`
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
cd honeyDueAPI-go/deploy-k3s-dev
|
||||
|
||||
# 1. Fill in config
|
||||
cp config.yaml.example config.yaml
|
||||
# Edit config.yaml — fill in ALL empty values
|
||||
|
||||
# 2. Create secret files (see secrets/README.md)
|
||||
echo "your-postgres-password" > secrets/postgres_password.txt
|
||||
openssl rand -base64 48 > secrets/secret_key.txt
|
||||
echo "your-smtp-password" > secrets/email_host_password.txt
|
||||
echo "your-fcm-key" > secrets/fcm_server_key.txt
|
||||
openssl rand -base64 24 > secrets/minio_root_password.txt
|
||||
cp /path/to/AuthKey.p8 secrets/apns_auth_key.p8
|
||||
|
||||
# 3. Install K3s → Create secrets → Deploy
|
||||
./scripts/01-setup-k3s.sh
|
||||
./scripts/02-setup-secrets.sh
|
||||
./scripts/03-deploy.sh
|
||||
|
||||
# 4. Point DNS at the server IP, then verify
|
||||
./scripts/04-verify.sh
|
||||
curl https://devapi.myhoneydue.com/api/health/
|
||||
```
|
||||
|
||||
## Prod vs Dev
|
||||
|
||||
| Component | Prod (`deploy-k3s/`) | Dev (`deploy-k3s-dev/`) |
|
||||
|---|---|---|
|
||||
| Nodes | 3x CX33 (HA etcd) | 1 node (any VPS) |
|
||||
| PostgreSQL | Neon (managed) | In-cluster container |
|
||||
| File storage | Backblaze B2 | MinIO (S3-compatible) |
|
||||
| Redis | In-cluster | In-cluster (identical) |
|
||||
| TLS | Cloudflare origin cert | Let's Encrypt (or Cloudflare) |
|
||||
| Replicas | api=3, worker=2 | All 1 |
|
||||
| HPA/PDB | Enabled | Not deployed |
|
||||
| Network policies | Same | Same + postgres/minio rules |
|
||||
| Security contexts | Same | Same (except postgres) |
|
||||
| Deploy workflow | Same scripts | Same scripts |
|
||||
| Docker images | Same | Same |
|
||||
|
||||
## TLS Modes
|
||||
|
||||
**Let's Encrypt** (default): Traefik auto-provisions certs. Set `tls.letsencrypt_email` in config.yaml.
|
||||
|
||||
**Cloudflare**: Same as prod. Set `tls.mode: cloudflare`, add origin cert files to `secrets/`.
|
||||
|
||||
## Storage Note
|
||||
|
||||
MinIO provides the same S3-compatible API as Backblaze B2. The Go API uses the same env vars (`B2_KEY_ID`, `B2_APP_KEY`, `B2_BUCKET_NAME`, `B2_ENDPOINT`) — it connects to MinIO instead of B2 without code changes.
|
||||
|
||||
An additional env var `STORAGE_USE_SSL=false` is set since MinIO runs in-cluster over HTTP. If the Go storage service hardcodes HTTPS, it may need a small change to respect this flag.
|
||||
|
||||
## Monitoring
|
||||
|
||||
```bash
|
||||
stern -n honeydue . # All logs
|
||||
kubectl logs -n honeydue deploy/api -f # API logs
|
||||
kubectl top pods -n honeydue # Resource usage
|
||||
```
|
||||
|
||||
## Rollback
|
||||
|
||||
```bash
|
||||
./scripts/rollback.sh
|
||||
```
|
||||
103
deploy-k3s-dev/config.yaml.example
Normal file
103
deploy-k3s-dev/config.yaml.example
Normal file
@@ -0,0 +1,103 @@
|
||||
# config.yaml — single source of truth for honeyDue K3s DEV deployment
|
||||
# Copy to config.yaml, fill in all empty values, then run scripts in order.
|
||||
# This file is gitignored — never commit it with real values.
|
||||
|
||||
# --- Server ---
|
||||
server:
|
||||
host: "" # Server IP or SSH config alias
|
||||
user: root # SSH user
|
||||
ssh_key: ~/.ssh/id_ed25519
|
||||
|
||||
# --- Domains ---
|
||||
domains:
|
||||
api: devapi.myhoneydue.com
|
||||
admin: devadmin.myhoneydue.com
|
||||
base: dev.myhoneydue.com
|
||||
|
||||
# --- Container Registry (GHCR) ---
|
||||
registry:
|
||||
server: ghcr.io
|
||||
namespace: "" # GitHub username or org
|
||||
username: "" # GitHub username
|
||||
token: "" # PAT with read:packages, write:packages
|
||||
|
||||
# --- Database (in-cluster PostgreSQL) ---
|
||||
database:
|
||||
name: honeydue_dev
|
||||
user: honeydue
|
||||
# password goes in secrets/postgres_password.txt
|
||||
max_open_conns: 10
|
||||
max_idle_conns: 5
|
||||
max_lifetime: "600s"
|
||||
|
||||
# --- Email (Fastmail) ---
|
||||
email:
|
||||
host: smtp.fastmail.com
|
||||
port: 587
|
||||
user: "" # Fastmail email address
|
||||
from: "honeyDue DEV <noreply@myhoneydue.com>"
|
||||
use_tls: true
|
||||
|
||||
# --- Push Notifications ---
|
||||
push:
|
||||
apns_key_id: ""
|
||||
apns_team_id: ""
|
||||
apns_topic: com.tt.honeyDue
|
||||
apns_production: false
|
||||
apns_use_sandbox: true # Sandbox for dev
|
||||
|
||||
# --- Object Storage (in-cluster MinIO — S3-compatible, replaces B2) ---
|
||||
storage:
|
||||
minio_root_user: honeydue # MinIO access key
|
||||
# minio_root_password goes in secrets/minio_root_password.txt
|
||||
bucket: honeydue-dev
|
||||
max_file_size: 10485760
|
||||
allowed_types: "image/jpeg,image/png,image/gif,image/webp,application/pdf"
|
||||
|
||||
# --- Worker Schedules (UTC hours) ---
|
||||
worker:
|
||||
task_reminder_hour: 14
|
||||
overdue_reminder_hour: 15
|
||||
daily_digest_hour: 3
|
||||
|
||||
# --- Feature Flags ---
|
||||
features:
|
||||
push_enabled: true
|
||||
email_enabled: false # Disabled for dev by default
|
||||
webhooks_enabled: false
|
||||
onboarding_emails_enabled: false
|
||||
pdf_reports_enabled: true
|
||||
worker_enabled: true
|
||||
|
||||
# --- Redis ---
|
||||
redis:
|
||||
password: "" # Set a strong password
|
||||
|
||||
# --- Admin Panel ---
|
||||
admin:
|
||||
basic_auth_user: "" # HTTP basic auth username
|
||||
basic_auth_password: "" # HTTP basic auth password
|
||||
|
||||
# --- TLS ---
|
||||
tls:
|
||||
mode: letsencrypt # "letsencrypt" or "cloudflare"
|
||||
letsencrypt_email: "" # Required if mode=letsencrypt
|
||||
# If mode=cloudflare, create secrets/cloudflare-origin.crt and .key
|
||||
|
||||
# --- Apple Auth / IAP (optional) ---
|
||||
apple_auth:
|
||||
client_id: ""
|
||||
team_id: ""
|
||||
iap_key_id: ""
|
||||
iap_issuer_id: ""
|
||||
iap_bundle_id: ""
|
||||
iap_key_path: ""
|
||||
iap_sandbox: true
|
||||
|
||||
# --- Google Auth / IAP (optional) ---
|
||||
google_auth:
|
||||
client_id: ""
|
||||
android_client_id: ""
|
||||
ios_client_id: ""
|
||||
iap_package_name: ""
|
||||
iap_service_account_path: ""
|
||||
94
deploy-k3s-dev/manifests/admin/deployment.yaml
Normal file
94
deploy-k3s-dev/manifests/admin/deployment.yaml
Normal file
@@ -0,0 +1,94 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: admin
|
||||
namespace: honeydue
|
||||
labels:
|
||||
app.kubernetes.io/name: admin
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 0
|
||||
maxSurge: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: admin
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: admin
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
spec:
|
||||
serviceAccountName: admin
|
||||
imagePullSecrets:
|
||||
- name: ghcr-credentials
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
runAsGroup: 1001
|
||||
fsGroup: 1001
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: admin
|
||||
image: IMAGE_PLACEHOLDER # Replaced by 03-deploy.sh
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
protocol: TCP
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
env:
|
||||
- name: PORT
|
||||
value: "3000"
|
||||
- name: HOSTNAME
|
||||
value: "0.0.0.0"
|
||||
- name: NEXT_PUBLIC_API_URL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: honeydue-config
|
||||
key: NEXT_PUBLIC_API_URL
|
||||
volumeMounts:
|
||||
- name: nextjs-cache
|
||||
mountPath: /app/.next/cache
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /admin/
|
||||
port: 3000
|
||||
failureThreshold: 12
|
||||
periodSeconds: 5
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /admin/
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /admin/
|
||||
port: 3000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 10
|
||||
volumes:
|
||||
- name: nextjs-cache
|
||||
emptyDir:
|
||||
sizeLimit: 256Mi
|
||||
- name: tmp
|
||||
emptyDir:
|
||||
sizeLimit: 64Mi
|
||||
16
deploy-k3s-dev/manifests/admin/service.yaml
Normal file
16
deploy-k3s-dev/manifests/admin/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: admin
|
||||
namespace: honeydue
|
||||
labels:
|
||||
app.kubernetes.io/name: admin
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: admin
|
||||
ports:
|
||||
- port: 3000
|
||||
targetPort: 3000
|
||||
protocol: TCP
|
||||
56
deploy-k3s-dev/manifests/ingress/ingress.yaml
Normal file
56
deploy-k3s-dev/manifests/ingress/ingress.yaml
Normal file
@@ -0,0 +1,56 @@
|
||||
# API Ingress — TLS via Let's Encrypt (default) or Cloudflare origin cert
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: honeydue-api
|
||||
namespace: honeydue
|
||||
labels:
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
annotations:
|
||||
# TLS_ANNOTATIONS_PLACEHOLDER — replaced by 03-deploy.sh based on tls.mode
|
||||
traefik.ingress.kubernetes.io/router.middlewares: honeydue-security-headers@kubernetescrd,honeydue-rate-limit@kubernetescrd
|
||||
spec:
|
||||
tls:
|
||||
- hosts:
|
||||
- API_DOMAIN_PLACEHOLDER
|
||||
secretName: TLS_SECRET_PLACEHOLDER
|
||||
rules:
|
||||
- host: API_DOMAIN_PLACEHOLDER
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: api
|
||||
port:
|
||||
number: 8000
|
||||
|
||||
---
|
||||
# Admin Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: honeydue-admin
|
||||
namespace: honeydue
|
||||
labels:
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
annotations:
|
||||
# TLS_ANNOTATIONS_PLACEHOLDER — replaced by 03-deploy.sh based on tls.mode
|
||||
traefik.ingress.kubernetes.io/router.middlewares: honeydue-security-headers@kubernetescrd,honeydue-rate-limit@kubernetescrd,honeydue-admin-auth@kubernetescrd
|
||||
spec:
|
||||
tls:
|
||||
- hosts:
|
||||
- ADMIN_DOMAIN_PLACEHOLDER
|
||||
secretName: TLS_SECRET_PLACEHOLDER
|
||||
rules:
|
||||
- host: ADMIN_DOMAIN_PLACEHOLDER
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: admin
|
||||
port:
|
||||
number: 3000
|
||||
45
deploy-k3s-dev/manifests/ingress/middleware.yaml
Normal file
45
deploy-k3s-dev/manifests/ingress/middleware.yaml
Normal file
@@ -0,0 +1,45 @@
|
||||
# Traefik CRD middleware for rate limiting
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: rate-limit
|
||||
namespace: honeydue
|
||||
spec:
|
||||
rateLimit:
|
||||
average: 100
|
||||
burst: 200
|
||||
period: 1m
|
||||
|
||||
---
|
||||
# Security headers
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: security-headers
|
||||
namespace: honeydue
|
||||
spec:
|
||||
headers:
|
||||
frameDeny: true
|
||||
contentTypeNosniff: true
|
||||
browserXssFilter: true
|
||||
referrerPolicy: "strict-origin-when-cross-origin"
|
||||
customResponseHeaders:
|
||||
X-Content-Type-Options: "nosniff"
|
||||
X-Frame-Options: "DENY"
|
||||
Strict-Transport-Security: "max-age=31536000; includeSubDomains"
|
||||
Content-Security-Policy: "default-src 'self'; frame-ancestors 'none'"
|
||||
Permissions-Policy: "camera=(), microphone=(), geolocation=()"
|
||||
X-Permitted-Cross-Domain-Policies: "none"
|
||||
|
||||
---
|
||||
# Admin basic auth — additional auth layer for admin panel
|
||||
# Secret created by 02-setup-secrets.sh from config.yaml credentials
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: admin-auth
|
||||
namespace: honeydue
|
||||
spec:
|
||||
basicAuth:
|
||||
secret: admin-basic-auth
|
||||
realm: "honeyDue Admin"
|
||||
81
deploy-k3s-dev/manifests/minio/create-bucket-job.yaml
Normal file
81
deploy-k3s-dev/manifests/minio/create-bucket-job.yaml
Normal file
@@ -0,0 +1,81 @@
|
||||
# One-shot job to create the default bucket in MinIO.
|
||||
# Applied by 03-deploy.sh after MinIO is running.
|
||||
# Re-running is safe — mc mb --ignore-existing is idempotent.
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: minio-create-bucket
|
||||
namespace: honeydue
|
||||
labels:
|
||||
app.kubernetes.io/name: minio
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 300
|
||||
backoffLimit: 5
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: minio-init
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: mc
|
||||
image: minio/mc:latest
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
echo "Waiting for MinIO to be ready..."
|
||||
until mc alias set honeydue http://minio.honeydue.svc.cluster.local:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" 2>/dev/null; do
|
||||
sleep 2
|
||||
done
|
||||
echo "Creating bucket: $BUCKET_NAME"
|
||||
mc mb --ignore-existing "honeydue/$BUCKET_NAME"
|
||||
echo "Bucket ready."
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
env:
|
||||
- name: MINIO_ROOT_USER
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: honeydue-config
|
||||
key: MINIO_ROOT_USER
|
||||
- name: MINIO_ROOT_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: honeydue-secrets
|
||||
key: MINIO_ROOT_PASSWORD
|
||||
- name: BUCKET_NAME
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: honeydue-config
|
||||
key: B2_BUCKET_NAME
|
||||
volumeMounts:
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: mc-config
|
||||
mountPath: /.mc
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 32Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 64Mi
|
||||
volumes:
|
||||
- name: tmp
|
||||
emptyDir:
|
||||
sizeLimit: 16Mi
|
||||
- name: mc-config
|
||||
emptyDir:
|
||||
sizeLimit: 16Mi
|
||||
restartPolicy: OnFailure
|
||||
89
deploy-k3s-dev/manifests/minio/deployment.yaml
Normal file
89
deploy-k3s-dev/manifests/minio/deployment.yaml
Normal file
@@ -0,0 +1,89 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: minio
|
||||
namespace: honeydue
|
||||
labels:
|
||||
app.kubernetes.io/name: minio
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate # ReadWriteOnce PVC — can't attach to two pods
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: minio
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: minio
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
spec:
|
||||
serviceAccountName: minio
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
fsGroup: 1000
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: minio
|
||||
image: minio/minio:latest
|
||||
args: ["server", "/data", "--console-address", ":9001"]
|
||||
ports:
|
||||
- name: api
|
||||
containerPort: 9000
|
||||
protocol: TCP
|
||||
- name: console
|
||||
containerPort: 9001
|
||||
protocol: TCP
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
env:
|
||||
- name: MINIO_ROOT_USER
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: honeydue-config
|
||||
key: MINIO_ROOT_USER
|
||||
- name: MINIO_ROOT_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: honeydue-secrets
|
||||
key: MINIO_ROOT_PASSWORD
|
||||
volumeMounts:
|
||||
- name: minio-data
|
||||
mountPath: /data
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /minio/health/ready
|
||||
port: 9000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /minio/health/live
|
||||
port: 9000
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
volumes:
|
||||
- name: minio-data
|
||||
persistentVolumeClaim:
|
||||
claimName: minio-data
|
||||
- name: tmp
|
||||
emptyDir:
|
||||
sizeLimit: 64Mi
|
||||
15
deploy-k3s-dev/manifests/minio/pvc.yaml
Normal file
15
deploy-k3s-dev/manifests/minio/pvc.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: minio-data
|
||||
namespace: honeydue
|
||||
labels:
|
||||
app.kubernetes.io/name: minio
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: local-path
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
21
deploy-k3s-dev/manifests/minio/service.yaml
Normal file
21
deploy-k3s-dev/manifests/minio/service.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: minio
|
||||
namespace: honeydue
|
||||
labels:
|
||||
app.kubernetes.io/name: minio
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: minio
|
||||
ports:
|
||||
- name: api
|
||||
port: 9000
|
||||
targetPort: 9000
|
||||
protocol: TCP
|
||||
- name: console
|
||||
port: 9001
|
||||
targetPort: 9001
|
||||
protocol: TCP
|
||||
6
deploy-k3s-dev/manifests/namespace.yaml
Normal file
6
deploy-k3s-dev/manifests/namespace.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: honeydue
|
||||
labels:
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
305
deploy-k3s-dev/manifests/network-policies.yaml
Normal file
305
deploy-k3s-dev/manifests/network-policies.yaml
Normal file
@@ -0,0 +1,305 @@
|
||||
# Network Policies — default-deny with explicit allows
|
||||
# Same pattern as prod, with added rules for in-cluster postgres and minio.
|
||||
|
||||
# --- Default deny all ingress and egress ---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: default-deny-all
|
||||
namespace: honeydue
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
|
||||
---
|
||||
# --- Allow DNS for all pods (required for service discovery) ---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-dns
|
||||
namespace: honeydue
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes:
|
||||
- Egress
|
||||
egress:
|
||||
- to: []
|
||||
ports:
|
||||
- protocol: UDP
|
||||
port: 53
|
||||
- protocol: TCP
|
||||
port: 53
|
||||
|
||||
---
|
||||
# --- API: allow ingress from Traefik (kube-system namespace) ---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-to-api
|
||||
namespace: honeydue
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: api
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8000
|
||||
|
||||
---
|
||||
# --- Admin: allow ingress from Traefik (kube-system namespace) ---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-to-admin
|
||||
namespace: honeydue
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: admin
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 3000
|
||||
|
||||
---
|
||||
# --- Redis: allow ingress ONLY from api + worker pods ---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-to-redis
|
||||
namespace: honeydue
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: redis
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: api
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: worker
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 6379
|
||||
|
||||
---
|
||||
# --- PostgreSQL: allow ingress ONLY from api + worker pods ---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-to-postgres
|
||||
namespace: honeydue
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: postgres
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: api
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: worker
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 5432
|
||||
|
||||
---
|
||||
# --- MinIO: allow ingress from api + worker + minio-init job pods ---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-to-minio
|
||||
namespace: honeydue
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: minio
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: api
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: worker
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: minio-init
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 9000
|
||||
- protocol: TCP
|
||||
port: 9001
|
||||
|
||||
---
|
||||
# --- API: allow egress to Redis, PostgreSQL, MinIO, external services ---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-egress-from-api
|
||||
namespace: honeydue
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: api
|
||||
policyTypes:
|
||||
- Egress
|
||||
egress:
|
||||
# Redis (in-cluster)
|
||||
- to:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: redis
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 6379
|
||||
# PostgreSQL (in-cluster)
|
||||
- to:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: postgres
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 5432
|
||||
# MinIO (in-cluster)
|
||||
- to:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: minio
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 9000
|
||||
# External services: SMTP (587), HTTPS (443 — APNs, FCM, PostHog)
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 0.0.0.0/0
|
||||
except:
|
||||
- 10.0.0.0/8
|
||||
- 172.16.0.0/12
|
||||
- 192.168.0.0/16
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 587
|
||||
- protocol: TCP
|
||||
port: 443
|
||||
|
||||
---
|
||||
# --- Worker: allow egress to Redis, PostgreSQL, MinIO, external services ---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-egress-from-worker
|
||||
namespace: honeydue
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: worker
|
||||
policyTypes:
|
||||
- Egress
|
||||
egress:
|
||||
# Redis (in-cluster)
|
||||
- to:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: redis
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 6379
|
||||
# PostgreSQL (in-cluster)
|
||||
- to:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: postgres
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 5432
|
||||
# MinIO (in-cluster)
|
||||
- to:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: minio
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 9000
|
||||
# External services: SMTP (587), HTTPS (443 — APNs, FCM)
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 0.0.0.0/0
|
||||
except:
|
||||
- 10.0.0.0/8
|
||||
- 172.16.0.0/12
|
||||
- 192.168.0.0/16
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 587
|
||||
- protocol: TCP
|
||||
port: 443
|
||||
|
||||
---
|
||||
# --- Admin: allow egress to API (internal) for SSR ---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-egress-from-admin
|
||||
namespace: honeydue
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: admin
|
||||
policyTypes:
|
||||
- Egress
|
||||
egress:
|
||||
- to:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: api
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8000
|
||||
|
||||
---
|
||||
# --- MinIO init job: allow egress to MinIO ---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-egress-from-minio-init
|
||||
namespace: honeydue
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: minio-init
|
||||
policyTypes:
|
||||
- Egress
|
||||
egress:
|
||||
- to:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: minio
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 9000
|
||||
93
deploy-k3s-dev/manifests/postgres/deployment.yaml
Normal file
93
deploy-k3s-dev/manifests/postgres/deployment.yaml
Normal file
@@ -0,0 +1,93 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: honeydue
|
||||
labels:
|
||||
app.kubernetes.io/name: postgres
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate # ReadWriteOnce PVC — can't attach to two pods
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: postgres
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
spec:
|
||||
serviceAccountName: postgres
|
||||
# Note: postgres image entrypoint requires root initially to set up
|
||||
# permissions, then drops to the postgres user. runAsNonRoot is not set
|
||||
# here because of this requirement. This differs from prod which uses
|
||||
# managed Neon PostgreSQL (no container to secure).
|
||||
securityContext:
|
||||
fsGroup: 999
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:17-alpine
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
protocol: TCP
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
env:
|
||||
- name: POSTGRES_DB
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: honeydue-config
|
||||
key: POSTGRES_DB
|
||||
- name: POSTGRES_USER
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: honeydue-config
|
||||
key: POSTGRES_USER
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: honeydue-secrets
|
||||
key: POSTGRES_PASSWORD
|
||||
- name: PGDATA
|
||||
value: /var/lib/postgresql/data/pgdata
|
||||
volumeMounts:
|
||||
- name: postgres-data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
- name: run
|
||||
mountPath: /var/run/postgresql
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: 1Gi
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["pg_isready", "-U", "honeydue"]
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: ["pg_isready", "-U", "honeydue"]
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
volumes:
|
||||
- name: postgres-data
|
||||
persistentVolumeClaim:
|
||||
claimName: postgres-data
|
||||
- name: run
|
||||
emptyDir: {}
|
||||
- name: tmp
|
||||
emptyDir:
|
||||
sizeLimit: 64Mi
|
||||
15
deploy-k3s-dev/manifests/postgres/pvc.yaml
Normal file
15
deploy-k3s-dev/manifests/postgres/pvc.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: postgres-data
|
||||
namespace: honeydue
|
||||
labels:
|
||||
app.kubernetes.io/name: postgres
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: local-path
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
16
deploy-k3s-dev/manifests/postgres/service.yaml
Normal file
16
deploy-k3s-dev/manifests/postgres/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: honeydue
|
||||
labels:
|
||||
app.kubernetes.io/name: postgres
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: postgres
|
||||
ports:
|
||||
- port: 5432
|
||||
targetPort: 5432
|
||||
protocol: TCP
|
||||
68
deploy-k3s-dev/manifests/rbac.yaml
Normal file
68
deploy-k3s-dev/manifests/rbac.yaml
Normal file
@@ -0,0 +1,68 @@
|
||||
# RBAC — Dedicated service accounts with no K8s API access
|
||||
# Each pod gets its own SA with automountServiceAccountToken: false,
|
||||
# so a compromised pod cannot query the Kubernetes API.
|
||||
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: api
|
||||
namespace: honeydue
|
||||
labels:
|
||||
app.kubernetes.io/name: api
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
automountServiceAccountToken: false
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: worker
|
||||
namespace: honeydue
|
||||
labels:
|
||||
app.kubernetes.io/name: worker
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
automountServiceAccountToken: false
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: admin
|
||||
namespace: honeydue
|
||||
labels:
|
||||
app.kubernetes.io/name: admin
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
automountServiceAccountToken: false
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: redis
|
||||
namespace: honeydue
|
||||
labels:
|
||||
app.kubernetes.io/name: redis
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
automountServiceAccountToken: false
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: honeydue
|
||||
labels:
|
||||
app.kubernetes.io/name: postgres
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
automountServiceAccountToken: false
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: minio
|
||||
namespace: honeydue
|
||||
labels:
|
||||
app.kubernetes.io/name: minio
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
automountServiceAccountToken: false
|
||||
105
deploy-k3s-dev/manifests/redis/deployment.yaml
Normal file
105
deploy-k3s-dev/manifests/redis/deployment.yaml
Normal file
@@ -0,0 +1,105 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
namespace: honeydue
|
||||
labels:
|
||||
app.kubernetes.io/name: redis
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate # ReadWriteOnce PVC — can't attach to two pods
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: redis
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
spec:
|
||||
serviceAccountName: redis
|
||||
# No nodeSelector — single node dev cluster
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 999
|
||||
runAsGroup: 999
|
||||
fsGroup: 999
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: redis
|
||||
image: redis:7-alpine
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
ARGS="--appendonly yes --appendfsync everysec --maxmemory 256mb --maxmemory-policy noeviction"
|
||||
if [ -n "$REDIS_PASSWORD" ]; then
|
||||
ARGS="$ARGS --requirepass $REDIS_PASSWORD"
|
||||
fi
|
||||
exec redis-server $ARGS
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
protocol: TCP
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
env:
|
||||
- name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: honeydue-secrets
|
||||
key: REDIS_PASSWORD
|
||||
optional: true
|
||||
volumeMounts:
|
||||
- name: redis-data
|
||||
mountPath: /data
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
if [ -n "$REDIS_PASSWORD" ]; then
|
||||
redis-cli -a "$REDIS_PASSWORD" ping 2>/dev/null | grep -q PONG
|
||||
else
|
||||
redis-cli ping | grep -q PONG
|
||||
fi
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
if [ -n "$REDIS_PASSWORD" ]; then
|
||||
redis-cli -a "$REDIS_PASSWORD" ping 2>/dev/null | grep -q PONG
|
||||
else
|
||||
redis-cli ping | grep -q PONG
|
||||
fi
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 5
|
||||
volumes:
|
||||
- name: redis-data
|
||||
persistentVolumeClaim:
|
||||
claimName: redis-data
|
||||
- name: tmp
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 64Mi
|
||||
15
deploy-k3s-dev/manifests/redis/pvc.yaml
Normal file
15
deploy-k3s-dev/manifests/redis/pvc.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: redis-data
|
||||
namespace: honeydue
|
||||
labels:
|
||||
app.kubernetes.io/name: redis
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: local-path
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
16
deploy-k3s-dev/manifests/redis/service.yaml
Normal file
16
deploy-k3s-dev/manifests/redis/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis
|
||||
namespace: honeydue
|
||||
labels:
|
||||
app.kubernetes.io/name: redis
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: redis
|
||||
ports:
|
||||
- port: 6379
|
||||
targetPort: 6379
|
||||
protocol: TCP
|
||||
16
deploy-k3s-dev/manifests/traefik/helmchartconfig.yaml
Normal file
16
deploy-k3s-dev/manifests/traefik/helmchartconfig.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
# Configure K3s's built-in Traefik with Let's Encrypt ACME.
|
||||
# Applied by 03-deploy.sh only when tls.mode=letsencrypt.
|
||||
# The email placeholder is replaced by the deploy script.
|
||||
apiVersion: helm.cattle.io/v1
|
||||
kind: HelmChartConfig
|
||||
metadata:
|
||||
name: traefik
|
||||
namespace: kube-system
|
||||
spec:
|
||||
valuesContent: |-
|
||||
additionalArguments:
|
||||
- "--certificatesresolvers.letsencrypt.acme.email=LETSENCRYPT_EMAIL_PLACEHOLDER"
|
||||
- "--certificatesresolvers.letsencrypt.acme.storage=/data/acme.json"
|
||||
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
|
||||
persistence:
|
||||
enabled: true
|
||||
235
deploy-k3s-dev/scripts/00-init.sh
Executable file
235
deploy-k3s-dev/scripts/00-init.sh
Executable file
@@ -0,0 +1,235 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DEPLOY_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
SECRETS_DIR="${DEPLOY_DIR}/secrets"
|
||||
CONFIG_FILE="${DEPLOY_DIR}/config.yaml"
|
||||
|
||||
log() { printf '[init] %s\n' "$*"; }
|
||||
warn() { printf '[init][warn] %s\n' "$*" >&2; }
|
||||
die() { printf '[init][error] %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
# --- Prerequisites ---
|
||||
|
||||
command -v openssl >/dev/null 2>&1 || die "Missing: openssl"
|
||||
command -v python3 >/dev/null 2>&1 || die "Missing: python3"
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " honeyDue Dev Server — Initial Setup"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "This script will:"
|
||||
echo " 1. Generate any missing random secrets"
|
||||
echo " 2. Ask for anything not already filled in"
|
||||
echo " 3. Create config.yaml with everything filled in"
|
||||
echo ""
|
||||
|
||||
mkdir -p "${SECRETS_DIR}"
|
||||
|
||||
# --- Generate random secrets (skip if already exist) ---
|
||||
|
||||
generate_if_missing() {
|
||||
local file="$1" label="$2" cmd="$3"
|
||||
if [[ -f "${file}" && -s "${file}" ]]; then
|
||||
log " ${label} — already exists, keeping"
|
||||
else
|
||||
eval "${cmd}" > "${file}"
|
||||
log " ${label} — generated"
|
||||
fi
|
||||
}
|
||||
|
||||
log "Checking secrets..."
|
||||
generate_if_missing "${SECRETS_DIR}/secret_key.txt" "secrets/secret_key.txt" "openssl rand -base64 48"
|
||||
generate_if_missing "${SECRETS_DIR}/postgres_password.txt" "secrets/postgres_password.txt" "openssl rand -base64 24"
|
||||
generate_if_missing "${SECRETS_DIR}/minio_root_password.txt" "secrets/minio_root_password.txt" "openssl rand -base64 24"
|
||||
generate_if_missing "${SECRETS_DIR}/email_host_password.txt" "secrets/email_host_password.txt" "echo PLACEHOLDER"
|
||||
log " secrets/fcm_server_key.txt — skipped (Android not ready)"
|
||||
generate_if_missing "${SECRETS_DIR}/apns_auth_key.p8" "secrets/apns_auth_key.p8" "echo ''"
|
||||
|
||||
REDIS_PW="$(openssl rand -base64 24)"
|
||||
log " Redis password — generated"
|
||||
|
||||
# --- Collect only what's missing ---
|
||||
|
||||
ask() {
|
||||
local var_name="$1" prompt="$2" default="${3:-}"
|
||||
local val
|
||||
if [[ -n "${default}" ]]; then
|
||||
read -rp "${prompt} [${default}]: " val
|
||||
val="${val:-${default}}"
|
||||
else
|
||||
read -rp "${prompt}: " val
|
||||
fi
|
||||
eval "${var_name}='${val}'"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "--- Server ---"
|
||||
ask SERVER_HOST "Server IP or SSH alias" "honeyDueDevUpdate"
|
||||
[[ -n "${SERVER_HOST}" ]] || die "Server host is required"
|
||||
ask SERVER_USER "SSH user" "root"
|
||||
ask SSH_KEY "SSH key path" "~/.ssh/id_ed25519"
|
||||
|
||||
echo ""
|
||||
echo "--- Container Registry (GHCR) ---"
|
||||
ask GHCR_USER "GitHub username" "treytartt"
|
||||
[[ -n "${GHCR_USER}" ]] || die "GitHub username is required"
|
||||
ask GHCR_TOKEN "GitHub PAT (read:packages, write:packages)" "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"
|
||||
[[ -n "${GHCR_TOKEN}" ]] || die "GitHub PAT is required"
|
||||
|
||||
echo ""
|
||||
echo "--- TLS ---"
|
||||
ask LE_EMAIL "Let's Encrypt email" "treytartt@fastmail.com"
|
||||
|
||||
echo ""
|
||||
echo "--- Admin Panel ---"
|
||||
ask ADMIN_USER "Admin basic auth username" "admin"
|
||||
ADMIN_PW="$(openssl rand -base64 16)"
|
||||
|
||||
# --- Known values from existing Dokku setup ---
|
||||
|
||||
EMAIL_USER="treytartt@fastmail.com"
|
||||
APNS_KEY_ID="9R5Q7ZX874"
|
||||
APNS_TEAM_ID="V3PF3M6B6U"
|
||||
|
||||
log ""
|
||||
log "Pre-filled from existing dev server:"
|
||||
log " Email user: ${EMAIL_USER}"
|
||||
log " APNS Key ID: ${APNS_KEY_ID}"
|
||||
log " APNS Team ID: ${APNS_TEAM_ID}"
|
||||
|
||||
# --- Generate config.yaml ---
|
||||
|
||||
log "Generating config.yaml..."
|
||||
|
||||
cat > "${CONFIG_FILE}" <<YAML
|
||||
# config.yaml — auto-generated by 00-init.sh
|
||||
# This file is gitignored — never commit it with real values.
|
||||
|
||||
# --- Server ---
|
||||
server:
|
||||
host: "${SERVER_HOST}"
|
||||
user: "${SERVER_USER}"
|
||||
ssh_key: "${SSH_KEY}"
|
||||
|
||||
# --- Domains ---
|
||||
domains:
|
||||
api: devapi.myhoneydue.com
|
||||
admin: devadmin.myhoneydue.com
|
||||
base: dev.myhoneydue.com
|
||||
|
||||
# --- Container Registry (GHCR) ---
|
||||
registry:
|
||||
server: ghcr.io
|
||||
namespace: "${GHCR_USER}"
|
||||
username: "${GHCR_USER}"
|
||||
token: "${GHCR_TOKEN}"
|
||||
|
||||
# --- Database (in-cluster PostgreSQL) ---
|
||||
database:
|
||||
name: honeydue_dev
|
||||
user: honeydue
|
||||
max_open_conns: 10
|
||||
max_idle_conns: 5
|
||||
max_lifetime: "600s"
|
||||
|
||||
# --- Email (Fastmail) ---
|
||||
email:
|
||||
host: smtp.fastmail.com
|
||||
port: 587
|
||||
user: "${EMAIL_USER}"
|
||||
from: "honeyDue DEV <${EMAIL_USER}>"
|
||||
use_tls: true
|
||||
|
||||
# --- Push Notifications ---
|
||||
push:
|
||||
apns_key_id: "${APNS_KEY_ID}"
|
||||
apns_team_id: "${APNS_TEAM_ID}"
|
||||
apns_topic: com.tt.honeyDue
|
||||
apns_production: false
|
||||
apns_use_sandbox: true
|
||||
|
||||
# --- Object Storage (in-cluster MinIO) ---
|
||||
storage:
|
||||
minio_root_user: honeydue
|
||||
bucket: honeydue-dev
|
||||
max_file_size: 10485760
|
||||
allowed_types: "image/jpeg,image/png,image/gif,image/webp,application/pdf"
|
||||
|
||||
# --- Worker Schedules (UTC hours) ---
|
||||
worker:
|
||||
task_reminder_hour: 14
|
||||
overdue_reminder_hour: 15
|
||||
daily_digest_hour: 3
|
||||
|
||||
# --- Feature Flags ---
|
||||
features:
|
||||
push_enabled: true
|
||||
email_enabled: false
|
||||
webhooks_enabled: false
|
||||
onboarding_emails_enabled: false
|
||||
pdf_reports_enabled: true
|
||||
worker_enabled: true
|
||||
|
||||
# --- Redis ---
|
||||
redis:
|
||||
password: "${REDIS_PW}"
|
||||
|
||||
# --- Admin Panel ---
|
||||
admin:
|
||||
basic_auth_user: "${ADMIN_USER}"
|
||||
basic_auth_password: "${ADMIN_PW}"
|
||||
|
||||
# --- TLS ---
|
||||
tls:
|
||||
mode: letsencrypt
|
||||
letsencrypt_email: "${LE_EMAIL}"
|
||||
|
||||
# --- Apple Auth / IAP ---
|
||||
apple_auth:
|
||||
client_id: "com.tt.honeyDue"
|
||||
team_id: "${APNS_TEAM_ID}"
|
||||
iap_key_id: ""
|
||||
iap_issuer_id: ""
|
||||
iap_bundle_id: ""
|
||||
iap_key_path: ""
|
||||
iap_sandbox: true
|
||||
|
||||
# --- Google Auth / IAP ---
|
||||
google_auth:
|
||||
client_id: ""
|
||||
android_client_id: ""
|
||||
ios_client_id: ""
|
||||
iap_package_name: ""
|
||||
iap_service_account_path: ""
|
||||
YAML
|
||||
|
||||
# --- Summary ---
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Setup Complete"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "Generated:"
|
||||
echo " config.yaml"
|
||||
echo " secrets/secret_key.txt"
|
||||
echo " secrets/postgres_password.txt"
|
||||
echo " secrets/minio_root_password.txt"
|
||||
echo " secrets/email_host_password.txt"
|
||||
echo " secrets/fcm_server_key.txt"
|
||||
echo " secrets/apns_auth_key.p8"
|
||||
echo ""
|
||||
echo "Admin panel credentials:"
|
||||
echo " Username: ${ADMIN_USER}"
|
||||
echo " Password: ${ADMIN_PW}"
|
||||
echo " (save these — they won't be shown again)"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " ./scripts/01-setup-k3s.sh"
|
||||
echo " ./scripts/02-setup-secrets.sh"
|
||||
echo " ./scripts/03-deploy.sh"
|
||||
echo " ./scripts/04-verify.sh"
|
||||
echo ""
|
||||
146
deploy-k3s-dev/scripts/01-setup-k3s.sh
Executable file
146
deploy-k3s-dev/scripts/01-setup-k3s.sh
Executable file
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=_config.sh
|
||||
source "${SCRIPT_DIR}/_config.sh"
|
||||
|
||||
log() { printf '[setup] %s\n' "$*"; }
|
||||
die() { printf '[setup][error] %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
# --- Local prerequisites ---
|
||||
|
||||
command -v kubectl >/dev/null 2>&1 || die "Missing locally: kubectl (https://kubernetes.io/docs/tasks/tools/)"
|
||||
|
||||
# --- Server connection ---
|
||||
|
||||
SERVER_HOST="$(cfg_require server.host "Server IP or SSH alias")"
|
||||
SERVER_USER="$(cfg server.user)"
|
||||
SERVER_USER="${SERVER_USER:-root}"
|
||||
SSH_KEY="$(cfg server.ssh_key | sed "s|~|${HOME}|g")"
|
||||
|
||||
SSH_OPTS=()
|
||||
if [[ -n "${SSH_KEY}" && -f "${SSH_KEY}" ]]; then
|
||||
SSH_OPTS+=(-i "${SSH_KEY}")
|
||||
fi
|
||||
SSH_OPTS+=(-o StrictHostKeyChecking=accept-new)
|
||||
|
||||
ssh_cmd() {
|
||||
ssh "${SSH_OPTS[@]}" "${SERVER_USER}@${SERVER_HOST}" "$@"
|
||||
}
|
||||
|
||||
log "Testing SSH connection to ${SERVER_USER}@${SERVER_HOST}..."
|
||||
ssh_cmd "echo 'SSH connection OK'" || die "Cannot SSH into ${SERVER_HOST}"
|
||||
|
||||
# --- Server prerequisites ---
|
||||
|
||||
log "Setting up server prerequisites..."
|
||||
|
||||
ssh_cmd 'bash -s' <<'REMOTE_SETUP'
|
||||
set -euo pipefail
|
||||
|
||||
log() { printf '[setup][remote] %s\n' "$*"; }
|
||||
|
||||
# --- System updates ---
|
||||
log "Updating system packages..."
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get upgrade -y -qq
|
||||
|
||||
# --- SSH hardening ---
|
||||
log "Hardening SSH..."
|
||||
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
|
||||
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config
|
||||
systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || true
|
||||
|
||||
# --- fail2ban ---
|
||||
if ! command -v fail2ban-client >/dev/null 2>&1; then
|
||||
log "Installing fail2ban..."
|
||||
apt-get install -y -qq fail2ban
|
||||
systemctl enable --now fail2ban
|
||||
else
|
||||
log "fail2ban already installed"
|
||||
fi
|
||||
|
||||
# --- Unattended security upgrades ---
|
||||
if ! dpkg -l | grep -q unattended-upgrades; then
|
||||
log "Installing unattended-upgrades..."
|
||||
apt-get install -y -qq unattended-upgrades
|
||||
dpkg-reconfigure -plow unattended-upgrades
|
||||
else
|
||||
log "unattended-upgrades already installed"
|
||||
fi
|
||||
|
||||
# --- Firewall (ufw) ---
|
||||
if command -v ufw >/dev/null 2>&1; then
|
||||
log "Configuring firewall..."
|
||||
ufw default deny incoming
|
||||
ufw default allow outgoing
|
||||
ufw allow 22/tcp # SSH
|
||||
ufw allow 443/tcp # HTTPS (Traefik)
|
||||
ufw allow 6443/tcp # K3s API
|
||||
ufw allow 80/tcp # HTTP (Let's Encrypt ACME challenge)
|
||||
ufw --force enable
|
||||
else
|
||||
log "Installing ufw..."
|
||||
apt-get install -y -qq ufw
|
||||
ufw default deny incoming
|
||||
ufw default allow outgoing
|
||||
ufw allow 22/tcp
|
||||
ufw allow 443/tcp
|
||||
ufw allow 6443/tcp
|
||||
ufw allow 80/tcp
|
||||
ufw --force enable
|
||||
fi
|
||||
|
||||
log "Server prerequisites complete."
|
||||
REMOTE_SETUP
|
||||
|
||||
# --- Install K3s ---
|
||||
|
||||
log "Installing K3s on ${SERVER_HOST}..."
|
||||
log " This takes about 1-2 minutes."
|
||||
|
||||
ssh_cmd "curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC='server --secrets-encryption' sh -"
|
||||
|
||||
# --- Wait for K3s to be ready ---
|
||||
|
||||
log "Waiting for K3s to be ready..."
|
||||
ssh_cmd "until kubectl get nodes >/dev/null 2>&1; do sleep 2; done"
|
||||
|
||||
# --- Copy kubeconfig ---
|
||||
|
||||
KUBECONFIG_PATH="${DEPLOY_DIR}/kubeconfig"
|
||||
|
||||
log "Copying kubeconfig..."
|
||||
ssh_cmd "sudo cat /etc/rancher/k3s/k3s.yaml" > "${KUBECONFIG_PATH}"
|
||||
|
||||
# Replace 127.0.0.1 with the server's actual IP/hostname
|
||||
# If SERVER_HOST is an SSH alias, resolve the actual IP
|
||||
ACTUAL_HOST="${SERVER_HOST}"
|
||||
if ! echo "${SERVER_HOST}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
# Try to resolve from SSH config
|
||||
RESOLVED="$(ssh -G "${SERVER_HOST}" 2>/dev/null | awk '/^hostname / {print $2}')"
|
||||
if [[ -n "${RESOLVED}" && "${RESOLVED}" != "${SERVER_HOST}" ]]; then
|
||||
ACTUAL_HOST="${RESOLVED}"
|
||||
fi
|
||||
fi
|
||||
|
||||
sed -i.bak "s|https://127.0.0.1:6443|https://${ACTUAL_HOST}:6443|g" "${KUBECONFIG_PATH}"
|
||||
rm -f "${KUBECONFIG_PATH}.bak"
|
||||
chmod 600 "${KUBECONFIG_PATH}"
|
||||
|
||||
# --- Verify ---
|
||||
|
||||
export KUBECONFIG="${KUBECONFIG_PATH}"
|
||||
log "Verifying cluster..."
|
||||
kubectl get nodes
|
||||
|
||||
log ""
|
||||
log "K3s installed successfully on ${SERVER_HOST}."
|
||||
log "Server hardened: SSH key-only, fail2ban, ufw firewall, unattended-upgrades."
|
||||
log ""
|
||||
log "Next steps:"
|
||||
log " export KUBECONFIG=${KUBECONFIG_PATH}"
|
||||
log " kubectl get nodes"
|
||||
log " ./scripts/02-setup-secrets.sh"
|
||||
153
deploy-k3s-dev/scripts/02-setup-secrets.sh
Executable file
153
deploy-k3s-dev/scripts/02-setup-secrets.sh
Executable file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=_config.sh
|
||||
source "${SCRIPT_DIR}/_config.sh"
|
||||
|
||||
SECRETS_DIR="${DEPLOY_DIR}/secrets"
|
||||
NAMESPACE="honeydue"
|
||||
|
||||
log() { printf '[secrets] %s\n' "$*"; }
|
||||
warn() { printf '[secrets][warn] %s\n' "$*" >&2; }
|
||||
die() { printf '[secrets][error] %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
# --- Prerequisites ---
|
||||
|
||||
command -v kubectl >/dev/null 2>&1 || die "Missing: kubectl"
|
||||
|
||||
kubectl get namespace "${NAMESPACE}" >/dev/null 2>&1 || {
|
||||
log "Creating namespace ${NAMESPACE}..."
|
||||
kubectl apply -f "${DEPLOY_DIR}/manifests/namespace.yaml"
|
||||
}
|
||||
|
||||
# --- Validate secret files ---
|
||||
|
||||
require_file() {
|
||||
local path="$1" label="$2"
|
||||
[[ -f "${path}" ]] || die "Missing: ${path} (${label})"
|
||||
[[ -s "${path}" ]] || die "Empty: ${path} (${label})"
|
||||
}
|
||||
|
||||
require_file "${SECRETS_DIR}/postgres_password.txt" "Postgres password"
|
||||
require_file "${SECRETS_DIR}/secret_key.txt" "SECRET_KEY"
|
||||
require_file "${SECRETS_DIR}/email_host_password.txt" "SMTP password"
|
||||
# FCM server key is optional (Android not yet ready)
|
||||
if [[ -f "${SECRETS_DIR}/fcm_server_key.txt" && -s "${SECRETS_DIR}/fcm_server_key.txt" ]]; then
|
||||
FCM_CONTENT="$(tr -d '\r\n' < "${SECRETS_DIR}/fcm_server_key.txt")"
|
||||
if [[ "${FCM_CONTENT}" == "PLACEHOLDER" ]]; then
|
||||
warn "fcm_server_key.txt is a placeholder — FCM push disabled"
|
||||
FCM_CONTENT=""
|
||||
fi
|
||||
else
|
||||
warn "fcm_server_key.txt not found — FCM push disabled"
|
||||
FCM_CONTENT=""
|
||||
fi
|
||||
require_file "${SECRETS_DIR}/apns_auth_key.p8" "APNS private key"
|
||||
require_file "${SECRETS_DIR}/minio_root_password.txt" "MinIO root password"
|
||||
|
||||
# Validate APNS key format
|
||||
if ! grep -q "BEGIN PRIVATE KEY" "${SECRETS_DIR}/apns_auth_key.p8"; then
|
||||
die "APNS key file does not look like a private key: ${SECRETS_DIR}/apns_auth_key.p8"
|
||||
fi
|
||||
|
||||
# Validate secret_key length (minimum 32 chars)
|
||||
SECRET_KEY_LEN="$(tr -d '\r\n' < "${SECRETS_DIR}/secret_key.txt" | wc -c | tr -d ' ')"
|
||||
if (( SECRET_KEY_LEN < 32 )); then
|
||||
die "secret_key.txt must be at least 32 characters (got ${SECRET_KEY_LEN})."
|
||||
fi
|
||||
|
||||
# Validate MinIO password length (minimum 8 chars)
|
||||
MINIO_PW_LEN="$(tr -d '\r\n' < "${SECRETS_DIR}/minio_root_password.txt" | wc -c | tr -d ' ')"
|
||||
if (( MINIO_PW_LEN < 8 )); then
|
||||
die "minio_root_password.txt must be at least 8 characters (got ${MINIO_PW_LEN})."
|
||||
fi
|
||||
|
||||
# --- Read optional config values ---
|
||||
|
||||
REDIS_PASSWORD="$(cfg redis.password 2>/dev/null || true)"
|
||||
ADMIN_AUTH_USER="$(cfg admin.basic_auth_user 2>/dev/null || true)"
|
||||
ADMIN_AUTH_PASSWORD="$(cfg admin.basic_auth_password 2>/dev/null || true)"
|
||||
TLS_MODE="$(cfg tls.mode 2>/dev/null || echo "letsencrypt")"
|
||||
|
||||
# --- Create app secrets ---
|
||||
|
||||
log "Creating honeydue-secrets..."
|
||||
SECRET_ARGS=(
|
||||
--namespace="${NAMESPACE}"
|
||||
--from-literal="POSTGRES_PASSWORD=$(tr -d '\r\n' < "${SECRETS_DIR}/postgres_password.txt")"
|
||||
--from-literal="SECRET_KEY=$(tr -d '\r\n' < "${SECRETS_DIR}/secret_key.txt")"
|
||||
--from-literal="EMAIL_HOST_PASSWORD=$(tr -d '\r\n' < "${SECRETS_DIR}/email_host_password.txt")"
|
||||
--from-literal="FCM_SERVER_KEY=${FCM_CONTENT}"
|
||||
--from-literal="MINIO_ROOT_PASSWORD=$(tr -d '\r\n' < "${SECRETS_DIR}/minio_root_password.txt")"
|
||||
)
|
||||
|
||||
if [[ -n "${REDIS_PASSWORD}" ]]; then
|
||||
log " Including REDIS_PASSWORD in secrets"
|
||||
SECRET_ARGS+=(--from-literal="REDIS_PASSWORD=${REDIS_PASSWORD}")
|
||||
fi
|
||||
|
||||
kubectl create secret generic honeydue-secrets \
|
||||
"${SECRET_ARGS[@]}" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
# --- Create APNS key secret ---
|
||||
|
||||
log "Creating honeydue-apns-key..."
|
||||
kubectl create secret generic honeydue-apns-key \
|
||||
--namespace="${NAMESPACE}" \
|
||||
--from-file="apns_auth_key.p8=${SECRETS_DIR}/apns_auth_key.p8" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
# --- Create GHCR registry credentials ---
|
||||
|
||||
REGISTRY_SERVER="$(cfg registry.server)"
|
||||
REGISTRY_USER="$(cfg registry.username)"
|
||||
REGISTRY_TOKEN="$(cfg registry.token)"
|
||||
|
||||
if [[ -n "${REGISTRY_SERVER}" && -n "${REGISTRY_USER}" && -n "${REGISTRY_TOKEN}" ]]; then
|
||||
log "Creating ghcr-credentials..."
|
||||
kubectl create secret docker-registry ghcr-credentials \
|
||||
--namespace="${NAMESPACE}" \
|
||||
--docker-server="${REGISTRY_SERVER}" \
|
||||
--docker-username="${REGISTRY_USER}" \
|
||||
--docker-password="${REGISTRY_TOKEN}" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
else
|
||||
warn "Registry credentials incomplete in config.yaml — skipping ghcr-credentials."
|
||||
fi
|
||||
|
||||
# --- Create Cloudflare origin cert (only if cloudflare mode) ---
|
||||
|
||||
if [[ "${TLS_MODE}" == "cloudflare" ]]; then
|
||||
require_file "${SECRETS_DIR}/cloudflare-origin.crt" "Cloudflare origin cert"
|
||||
require_file "${SECRETS_DIR}/cloudflare-origin.key" "Cloudflare origin key"
|
||||
|
||||
log "Creating cloudflare-origin-cert..."
|
||||
kubectl create secret tls cloudflare-origin-cert \
|
||||
--namespace="${NAMESPACE}" \
|
||||
--cert="${SECRETS_DIR}/cloudflare-origin.crt" \
|
||||
--key="${SECRETS_DIR}/cloudflare-origin.key" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
fi
|
||||
|
||||
# --- Create admin basic auth secret ---
|
||||
|
||||
if [[ -n "${ADMIN_AUTH_USER}" && -n "${ADMIN_AUTH_PASSWORD}" ]]; then
|
||||
command -v htpasswd >/dev/null 2>&1 || die "Missing: htpasswd (install apache2-utils)"
|
||||
log "Creating admin-basic-auth secret..."
|
||||
HTPASSWD="$(htpasswd -nb "${ADMIN_AUTH_USER}" "${ADMIN_AUTH_PASSWORD}")"
|
||||
kubectl create secret generic admin-basic-auth \
|
||||
--namespace="${NAMESPACE}" \
|
||||
--from-literal=users="${HTPASSWD}" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
else
|
||||
warn "admin.basic_auth_user/password not set in config.yaml — skipping admin-basic-auth."
|
||||
warn "Admin panel will NOT have basic auth protection."
|
||||
fi
|
||||
|
||||
# --- Done ---
|
||||
|
||||
log ""
|
||||
log "All secrets created in namespace '${NAMESPACE}'."
|
||||
log "Verify: kubectl get secrets -n ${NAMESPACE}"
|
||||
193
deploy-k3s-dev/scripts/03-deploy.sh
Executable file
193
deploy-k3s-dev/scripts/03-deploy.sh
Executable file
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=_config.sh
|
||||
source "${SCRIPT_DIR}/_config.sh"
|
||||
|
||||
REPO_DIR="$(cd "${DEPLOY_DIR}/.." && pwd)"
|
||||
NAMESPACE="honeydue"
|
||||
MANIFESTS="${DEPLOY_DIR}/manifests"
|
||||
|
||||
log() { printf '[deploy] %s\n' "$*"; }
|
||||
warn() { printf '[deploy][warn] %s\n' "$*" >&2; }
|
||||
die() { printf '[deploy][error] %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
# --- Parse arguments ---
|
||||
|
||||
SKIP_BUILD=false
|
||||
DEPLOY_TAG=""
|
||||
|
||||
while (( $# > 0 )); do
|
||||
case "$1" in
|
||||
--skip-build) SKIP_BUILD=true; shift ;;
|
||||
--tag)
|
||||
[[ -n "${2:-}" ]] || die "--tag requires a value"
|
||||
DEPLOY_TAG="$2"; shift 2 ;;
|
||||
-h|--help)
|
||||
cat <<'EOF'
|
||||
Usage: ./scripts/03-deploy.sh [OPTIONS]
|
||||
|
||||
Options:
|
||||
--skip-build Skip Docker build/push, use existing images
|
||||
--tag <tag> Image tag (default: git short SHA)
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
exit 0 ;;
|
||||
*) die "Unknown argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- Prerequisites ---
|
||||
|
||||
command -v kubectl >/dev/null 2>&1 || die "Missing: kubectl"
|
||||
command -v docker >/dev/null 2>&1 || die "Missing: docker"
|
||||
|
||||
if [[ -z "${DEPLOY_TAG}" ]]; then
|
||||
DEPLOY_TAG="$(git -C "${REPO_DIR}" rev-parse --short HEAD 2>/dev/null || echo "latest")"
|
||||
fi
|
||||
|
||||
# --- Read config ---
|
||||
|
||||
REGISTRY_SERVER="$(cfg_require registry.server "Container registry server")"
|
||||
REGISTRY_NS="$(cfg_require registry.namespace "Registry namespace")"
|
||||
REGISTRY_USER="$(cfg_require registry.username "Registry username")"
|
||||
REGISTRY_TOKEN="$(cfg_require registry.token "Registry token")"
|
||||
TLS_MODE="$(cfg tls.mode 2>/dev/null || echo "letsencrypt")"
|
||||
API_DOMAIN="$(cfg_require domains.api "API domain")"
|
||||
ADMIN_DOMAIN="$(cfg_require domains.admin "Admin domain")"
|
||||
|
||||
REGISTRY_PREFIX="${REGISTRY_SERVER%/}/${REGISTRY_NS#/}"
|
||||
API_IMAGE="${REGISTRY_PREFIX}/honeydue-api:${DEPLOY_TAG}"
|
||||
WORKER_IMAGE="${REGISTRY_PREFIX}/honeydue-worker:${DEPLOY_TAG}"
|
||||
ADMIN_IMAGE="${REGISTRY_PREFIX}/honeydue-admin:${DEPLOY_TAG}"
|
||||
|
||||
# --- Build and push ---
|
||||
|
||||
if [[ "${SKIP_BUILD}" == "false" ]]; then
|
||||
log "Logging in to ${REGISTRY_SERVER}..."
|
||||
printf '%s' "${REGISTRY_TOKEN}" | docker login "${REGISTRY_SERVER}" -u "${REGISTRY_USER}" --password-stdin >/dev/null
|
||||
|
||||
log "Building API image: ${API_IMAGE}"
|
||||
docker build --target api -t "${API_IMAGE}" "${REPO_DIR}"
|
||||
|
||||
log "Building Worker image: ${WORKER_IMAGE}"
|
||||
docker build --target worker -t "${WORKER_IMAGE}" "${REPO_DIR}"
|
||||
|
||||
log "Building Admin image: ${ADMIN_IMAGE}"
|
||||
docker build --target admin -t "${ADMIN_IMAGE}" "${REPO_DIR}"
|
||||
|
||||
log "Pushing images..."
|
||||
docker push "${API_IMAGE}"
|
||||
docker push "${WORKER_IMAGE}"
|
||||
docker push "${ADMIN_IMAGE}"
|
||||
|
||||
# Also tag and push :latest
|
||||
docker tag "${API_IMAGE}" "${REGISTRY_PREFIX}/honeydue-api:latest"
|
||||
docker tag "${WORKER_IMAGE}" "${REGISTRY_PREFIX}/honeydue-worker:latest"
|
||||
docker tag "${ADMIN_IMAGE}" "${REGISTRY_PREFIX}/honeydue-admin:latest"
|
||||
docker push "${REGISTRY_PREFIX}/honeydue-api:latest"
|
||||
docker push "${REGISTRY_PREFIX}/honeydue-worker:latest"
|
||||
docker push "${REGISTRY_PREFIX}/honeydue-admin:latest"
|
||||
else
|
||||
warn "Skipping build. Using images for tag: ${DEPLOY_TAG}"
|
||||
fi
|
||||
|
||||
# --- Generate and apply ConfigMap from config.yaml ---
|
||||
|
||||
log "Generating env from config.yaml..."
|
||||
ENV_FILE="$(mktemp)"
|
||||
trap 'rm -f "${ENV_FILE}"' EXIT
|
||||
generate_env > "${ENV_FILE}"
|
||||
|
||||
log "Creating ConfigMap..."
|
||||
kubectl create configmap honeydue-config \
|
||||
--namespace="${NAMESPACE}" \
|
||||
--from-env-file="${ENV_FILE}" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
# --- Configure TLS ---
|
||||
|
||||
if [[ "${TLS_MODE}" == "letsencrypt" ]]; then
|
||||
LE_EMAIL="$(cfg_require tls.letsencrypt_email "Let's Encrypt email")"
|
||||
|
||||
log "Configuring Traefik with Let's Encrypt (${LE_EMAIL})..."
|
||||
sed "s|LETSENCRYPT_EMAIL_PLACEHOLDER|${LE_EMAIL}|" \
|
||||
"${MANIFESTS}/traefik/helmchartconfig.yaml" | kubectl apply -f -
|
||||
|
||||
TLS_SECRET="letsencrypt-cert"
|
||||
TLS_ANNOTATION="traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt"
|
||||
elif [[ "${TLS_MODE}" == "cloudflare" ]]; then
|
||||
log "Using Cloudflare origin cert for TLS..."
|
||||
TLS_SECRET="cloudflare-origin-cert"
|
||||
TLS_ANNOTATION=""
|
||||
else
|
||||
die "Unknown tls.mode: ${TLS_MODE} (expected: letsencrypt or cloudflare)"
|
||||
fi
|
||||
|
||||
# --- Apply manifests ---
|
||||
|
||||
log "Applying manifests..."
|
||||
|
||||
kubectl apply -f "${MANIFESTS}/namespace.yaml"
|
||||
kubectl apply -f "${MANIFESTS}/rbac.yaml"
|
||||
kubectl apply -f "${MANIFESTS}/postgres/"
|
||||
kubectl apply -f "${MANIFESTS}/redis/"
|
||||
kubectl apply -f "${MANIFESTS}/minio/deployment.yaml"
|
||||
kubectl apply -f "${MANIFESTS}/minio/pvc.yaml"
|
||||
kubectl apply -f "${MANIFESTS}/minio/service.yaml"
|
||||
kubectl apply -f "${MANIFESTS}/ingress/middleware.yaml"
|
||||
|
||||
# Apply ingress with domain and TLS substitution
|
||||
sed -e "s|API_DOMAIN_PLACEHOLDER|${API_DOMAIN}|g" \
|
||||
-e "s|ADMIN_DOMAIN_PLACEHOLDER|${ADMIN_DOMAIN}|g" \
|
||||
-e "s|TLS_SECRET_PLACEHOLDER|${TLS_SECRET}|g" \
|
||||
-e "s|# TLS_ANNOTATIONS_PLACEHOLDER|${TLS_ANNOTATION}|g" \
|
||||
"${MANIFESTS}/ingress/ingress.yaml" | kubectl apply -f -
|
||||
|
||||
# Apply app deployments with image substitution
|
||||
sed "s|image: IMAGE_PLACEHOLDER|image: ${API_IMAGE}|" "${MANIFESTS}/api/deployment.yaml" | kubectl apply -f -
|
||||
kubectl apply -f "${MANIFESTS}/api/service.yaml"
|
||||
|
||||
sed "s|image: IMAGE_PLACEHOLDER|image: ${WORKER_IMAGE}|" "${MANIFESTS}/worker/deployment.yaml" | kubectl apply -f -
|
||||
|
||||
sed "s|image: IMAGE_PLACEHOLDER|image: ${ADMIN_IMAGE}|" "${MANIFESTS}/admin/deployment.yaml" | kubectl apply -f -
|
||||
kubectl apply -f "${MANIFESTS}/admin/service.yaml"
|
||||
|
||||
# Apply network policies
|
||||
kubectl apply -f "${MANIFESTS}/network-policies.yaml"
|
||||
|
||||
# --- Wait for infrastructure rollouts ---
|
||||
|
||||
log "Waiting for infrastructure rollouts..."
|
||||
kubectl rollout status deployment/postgres -n "${NAMESPACE}" --timeout=120s
|
||||
kubectl rollout status deployment/redis -n "${NAMESPACE}" --timeout=120s
|
||||
kubectl rollout status deployment/minio -n "${NAMESPACE}" --timeout=120s
|
||||
|
||||
# --- Create MinIO bucket ---
|
||||
|
||||
log "Creating MinIO bucket..."
|
||||
# Delete previous job run if it exists (jobs are immutable)
|
||||
kubectl delete job minio-create-bucket -n "${NAMESPACE}" 2>/dev/null || true
|
||||
kubectl apply -f "${MANIFESTS}/minio/create-bucket-job.yaml"
|
||||
kubectl wait --for=condition=complete job/minio-create-bucket -n "${NAMESPACE}" --timeout=120s
|
||||
|
||||
# --- Wait for app rollouts ---
|
||||
|
||||
log "Waiting for app rollouts..."
|
||||
kubectl rollout status deployment/api -n "${NAMESPACE}" --timeout=300s
|
||||
kubectl rollout status deployment/worker -n "${NAMESPACE}" --timeout=300s
|
||||
kubectl rollout status deployment/admin -n "${NAMESPACE}" --timeout=300s
|
||||
|
||||
# --- Done ---
|
||||
|
||||
log ""
|
||||
log "Deploy completed successfully."
|
||||
log "Tag: ${DEPLOY_TAG}"
|
||||
log "TLS: ${TLS_MODE}"
|
||||
log "Images:"
|
||||
log " API: ${API_IMAGE}"
|
||||
log " Worker: ${WORKER_IMAGE}"
|
||||
log " Admin: ${ADMIN_IMAGE}"
|
||||
log ""
|
||||
log "Run ./scripts/04-verify.sh to check cluster health."
|
||||
161
deploy-k3s-dev/scripts/04-verify.sh
Executable file
161
deploy-k3s-dev/scripts/04-verify.sh
Executable file
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
NAMESPACE="honeydue"
|
||||
|
||||
log() { printf '[verify] %s\n' "$*"; }
|
||||
sep() { printf '\n%s\n' "--- $1 ---"; }
|
||||
ok() { printf '[verify] ✓ %s\n' "$*"; }
|
||||
fail() { printf '[verify] ✗ %s\n' "$*"; }
|
||||
|
||||
command -v kubectl >/dev/null 2>&1 || { echo "Missing: kubectl" >&2; exit 1; }
|
||||
|
||||
sep "Node"
|
||||
kubectl get nodes -o wide
|
||||
|
||||
sep "Pods"
|
||||
kubectl get pods -n "${NAMESPACE}" -o wide
|
||||
|
||||
sep "Services"
|
||||
kubectl get svc -n "${NAMESPACE}"
|
||||
|
||||
sep "Ingress"
|
||||
kubectl get ingress -n "${NAMESPACE}"
|
||||
|
||||
sep "PVCs"
|
||||
kubectl get pvc -n "${NAMESPACE}"
|
||||
|
||||
sep "Secrets (names only)"
|
||||
kubectl get secrets -n "${NAMESPACE}"
|
||||
|
||||
sep "ConfigMap keys"
|
||||
kubectl get configmap honeydue-config -n "${NAMESPACE}" -o jsonpath='{.data}' 2>/dev/null | python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
for k in sorted(d.keys()):
|
||||
v = d[k]
|
||||
if any(s in k.upper() for s in ['PASSWORD', 'SECRET', 'TOKEN', 'KEY']):
|
||||
v = '***REDACTED***'
|
||||
print(f' {k}={v}')
|
||||
except:
|
||||
print(' (could not parse)')
|
||||
" 2>/dev/null || log "ConfigMap not found or not parseable"
|
||||
|
||||
sep "Warning Events (last 15 min)"
|
||||
kubectl get events -n "${NAMESPACE}" --field-selector type=Warning --sort-by='.lastTimestamp' 2>/dev/null | tail -20 || log "No warning events"
|
||||
|
||||
sep "Pod Restart Counts"
|
||||
kubectl get pods -n "${NAMESPACE}" -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{range .status.containerStatuses[*]}{.restartCount}{end}{"\n"}{end}' 2>/dev/null || true
|
||||
|
||||
# =============================================================================
|
||||
# Infrastructure Health
|
||||
# =============================================================================
|
||||
|
||||
sep "PostgreSQL Health"
|
||||
PG_POD="$(kubectl get pods -n "${NAMESPACE}" -l app.kubernetes.io/name=postgres -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)"
|
||||
if [[ -n "${PG_POD}" ]]; then
|
||||
kubectl exec -n "${NAMESPACE}" "${PG_POD}" -- pg_isready -U honeydue 2>/dev/null && ok "PostgreSQL is ready" || fail "PostgreSQL is NOT ready"
|
||||
else
|
||||
fail "No PostgreSQL pod found"
|
||||
fi
|
||||
|
||||
sep "Redis Health"
|
||||
REDIS_POD="$(kubectl get pods -n "${NAMESPACE}" -l app.kubernetes.io/name=redis -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)"
|
||||
if [[ -n "${REDIS_POD}" ]]; then
|
||||
kubectl exec -n "${NAMESPACE}" "${REDIS_POD}" -- sh -c 'if [ -n "$REDIS_PASSWORD" ]; then redis-cli -a "$REDIS_PASSWORD" ping 2>/dev/null; else redis-cli ping; fi' 2>/dev/null | grep -q PONG && ok "Redis is ready" || fail "Redis is NOT ready"
|
||||
else
|
||||
fail "No Redis pod found"
|
||||
fi
|
||||
|
||||
sep "MinIO Health"
|
||||
MINIO_POD="$(kubectl get pods -n "${NAMESPACE}" -l app.kubernetes.io/name=minio -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)"
|
||||
if [[ -n "${MINIO_POD}" ]]; then
|
||||
kubectl exec -n "${NAMESPACE}" "${MINIO_POD}" -- curl -sf http://localhost:9000/minio/health/ready 2>/dev/null && ok "MinIO is ready" || fail "MinIO is NOT ready"
|
||||
else
|
||||
fail "No MinIO pod found"
|
||||
fi
|
||||
|
||||
sep "API Health Check"
|
||||
API_POD="$(kubectl get pods -n "${NAMESPACE}" -l app.kubernetes.io/name=api -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)"
|
||||
if [[ -n "${API_POD}" ]]; then
|
||||
kubectl exec -n "${NAMESPACE}" "${API_POD}" -- curl -sf http://localhost:8000/api/health/ 2>/dev/null && ok "API health check passed" || fail "API health check FAILED"
|
||||
else
|
||||
fail "No API pod found"
|
||||
fi
|
||||
|
||||
sep "Resource Usage"
|
||||
kubectl top pods -n "${NAMESPACE}" 2>/dev/null || log "Metrics server not available (install with: kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml)"
|
||||
|
||||
# =============================================================================
|
||||
# Security Verification
|
||||
# =============================================================================
|
||||
|
||||
sep "Security: Network Policies"
|
||||
NP_COUNT="$(kubectl get networkpolicy -n "${NAMESPACE}" --no-headers 2>/dev/null | wc -l | tr -d ' ')"
|
||||
if (( NP_COUNT >= 5 )); then
|
||||
ok "Found ${NP_COUNT} network policies"
|
||||
kubectl get networkpolicy -n "${NAMESPACE}" --no-headers 2>/dev/null | while read -r line; do
|
||||
echo " ${line}"
|
||||
done
|
||||
else
|
||||
fail "Expected 5+ network policies, found ${NP_COUNT}"
|
||||
fi
|
||||
|
||||
sep "Security: Service Accounts"
|
||||
SA_COUNT="$(kubectl get sa -n "${NAMESPACE}" --no-headers 2>/dev/null | grep -cv default | tr -d ' ')"
|
||||
if (( SA_COUNT >= 6 )); then
|
||||
ok "Found ${SA_COUNT} custom service accounts (api, worker, admin, redis, postgres, minio)"
|
||||
else
|
||||
fail "Expected 6 custom service accounts, found ${SA_COUNT}"
|
||||
fi
|
||||
kubectl get sa -n "${NAMESPACE}" --no-headers 2>/dev/null | while read -r line; do
|
||||
echo " ${line}"
|
||||
done
|
||||
|
||||
sep "Security: Pod Security Contexts"
|
||||
PODS_WITHOUT_SECURITY="$(kubectl get pods -n "${NAMESPACE}" -o json 2>/dev/null | python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
issues = []
|
||||
for pod in data.get('items', []):
|
||||
name = pod['metadata']['name']
|
||||
spec = pod['spec']
|
||||
sc = spec.get('securityContext', {})
|
||||
# Postgres is exempt from runAsNonRoot (entrypoint requirement)
|
||||
is_postgres = any('postgres' in c.get('image', '') for c in spec.get('containers', []))
|
||||
if not sc.get('runAsNonRoot') and not is_postgres:
|
||||
issues.append(f'{name}: missing runAsNonRoot')
|
||||
for c in spec.get('containers', []):
|
||||
csc = c.get('securityContext', {})
|
||||
if csc.get('allowPrivilegeEscalation', True):
|
||||
issues.append(f'{name}/{c[\"name\"]}: allowPrivilegeEscalation not false')
|
||||
if issues:
|
||||
for i in issues:
|
||||
print(i)
|
||||
else:
|
||||
print('OK')
|
||||
except Exception as e:
|
||||
print(f'Error: {e}')
|
||||
" 2>/dev/null || echo "Error parsing pod specs")"
|
||||
|
||||
if [[ "${PODS_WITHOUT_SECURITY}" == "OK" ]]; then
|
||||
ok "All pods have proper security contexts"
|
||||
else
|
||||
fail "Pod security context issues:"
|
||||
echo "${PODS_WITHOUT_SECURITY}" | while read -r line; do
|
||||
echo " ${line}"
|
||||
done
|
||||
fi
|
||||
|
||||
sep "Security: Admin Basic Auth"
|
||||
ADMIN_AUTH="$(kubectl get secret admin-basic-auth -n "${NAMESPACE}" -o name 2>/dev/null || true)"
|
||||
if [[ -n "${ADMIN_AUTH}" ]]; then
|
||||
ok "admin-basic-auth secret exists"
|
||||
else
|
||||
fail "admin-basic-auth secret not found — admin panel has no additional auth layer"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
log "Verification complete."
|
||||
152
deploy-k3s-dev/scripts/_config.sh
Executable file
152
deploy-k3s-dev/scripts/_config.sh
Executable file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared config helper — sourced by all deploy scripts.
|
||||
# Provides cfg() to read values from config.yaml.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DEPLOY_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
CONFIG_FILE="${DEPLOY_DIR}/config.yaml"
|
||||
|
||||
if [[ ! -f "${CONFIG_FILE}" ]]; then
|
||||
if [[ -f "${CONFIG_FILE}.example" ]]; then
|
||||
echo "[error] config.yaml not found. Run: cp config.yaml.example config.yaml" >&2
|
||||
else
|
||||
echo "[error] config.yaml not found." >&2
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# cfg "dotted.key.path" — reads a value from config.yaml
|
||||
cfg() {
|
||||
python3 -c "
|
||||
import yaml, json, sys
|
||||
with open(sys.argv[1]) as f:
|
||||
c = yaml.safe_load(f)
|
||||
keys = sys.argv[2].split('.')
|
||||
v = c
|
||||
for k in keys:
|
||||
if isinstance(v, list):
|
||||
v = v[int(k)]
|
||||
else:
|
||||
v = v[k]
|
||||
if isinstance(v, bool):
|
||||
print(str(v).lower())
|
||||
elif isinstance(v, (dict, list)):
|
||||
print(json.dumps(v))
|
||||
else:
|
||||
print('' if v is None else v)
|
||||
" "${CONFIG_FILE}" "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# cfg_require "key" "label" — reads value and dies if empty
|
||||
cfg_require() {
|
||||
local val
|
||||
val="$(cfg "$1")"
|
||||
if [[ -z "${val}" ]]; then
|
||||
echo "[error] Missing required config: $1 ($2)" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s' "${val}"
|
||||
}
|
||||
|
||||
# generate_env — writes the flat env file the app expects to stdout
|
||||
# Points DB at in-cluster PostgreSQL, storage at in-cluster MinIO
|
||||
generate_env() {
|
||||
python3 -c "
|
||||
import yaml
|
||||
|
||||
with open('${CONFIG_FILE}') as f:
|
||||
c = yaml.safe_load(f)
|
||||
|
||||
d = c['domains']
|
||||
db = c['database']
|
||||
em = c['email']
|
||||
ps = c['push']
|
||||
st = c['storage']
|
||||
wk = c['worker']
|
||||
ft = c['features']
|
||||
aa = c.get('apple_auth', {})
|
||||
ga = c.get('google_auth', {})
|
||||
rd = c.get('redis', {})
|
||||
|
||||
def b(v):
|
||||
return str(v).lower() if isinstance(v, bool) else str(v)
|
||||
|
||||
def val(v):
|
||||
return '' if v is None else str(v)
|
||||
|
||||
lines = [
|
||||
# API
|
||||
'DEBUG=true',
|
||||
f\"ALLOWED_HOSTS={d['api']},{d['base']},localhost\",
|
||||
f\"CORS_ALLOWED_ORIGINS=https://{d['base']},https://{d['admin']}\",
|
||||
'TIMEZONE=UTC',
|
||||
f\"BASE_URL=https://{d['base']}\",
|
||||
'PORT=8000',
|
||||
# Admin
|
||||
f\"NEXT_PUBLIC_API_URL=https://{d['api']}\",
|
||||
f\"ADMIN_PANEL_URL=https://{d['admin']}\",
|
||||
# Database (in-cluster PostgreSQL)
|
||||
'DB_HOST=postgres.honeydue.svc.cluster.local',
|
||||
'DB_PORT=5432',
|
||||
f\"POSTGRES_USER={val(db['user'])}\",
|
||||
f\"POSTGRES_DB={db['name']}\",
|
||||
'DB_SSLMODE=disable',
|
||||
f\"DB_MAX_OPEN_CONNS={db['max_open_conns']}\",
|
||||
f\"DB_MAX_IDLE_CONNS={db['max_idle_conns']}\",
|
||||
f\"DB_MAX_LIFETIME={db['max_lifetime']}\",
|
||||
# Redis (in-cluster)
|
||||
f\"REDIS_URL=redis://{':%s@' % val(rd.get('password')) if rd.get('password') else ''}redis.honeydue.svc.cluster.local:6379/0\",
|
||||
'REDIS_DB=0',
|
||||
# Email
|
||||
f\"EMAIL_HOST={em['host']}\",
|
||||
f\"EMAIL_PORT={em['port']}\",
|
||||
f\"EMAIL_USE_TLS={b(em['use_tls'])}\",
|
||||
f\"EMAIL_HOST_USER={val(em['user'])}\",
|
||||
f\"DEFAULT_FROM_EMAIL={val(em['from'])}\",
|
||||
# Push
|
||||
'APNS_AUTH_KEY_PATH=/secrets/apns/apns_auth_key.p8',
|
||||
f\"APNS_AUTH_KEY_ID={val(ps['apns_key_id'])}\",
|
||||
f\"APNS_TEAM_ID={val(ps['apns_team_id'])}\",
|
||||
f\"APNS_TOPIC={ps['apns_topic']}\",
|
||||
f\"APNS_USE_SANDBOX={b(ps['apns_use_sandbox'])}\",
|
||||
f\"APNS_PRODUCTION={b(ps['apns_production'])}\",
|
||||
# Worker
|
||||
f\"TASK_REMINDER_HOUR={wk['task_reminder_hour']}\",
|
||||
f\"OVERDUE_REMINDER_HOUR={wk['overdue_reminder_hour']}\",
|
||||
f\"DAILY_DIGEST_HOUR={wk['daily_digest_hour']}\",
|
||||
# Storage (in-cluster MinIO — S3-compatible, same env vars as B2)
|
||||
f\"B2_KEY_ID={val(st['minio_root_user'])}\",
|
||||
# B2_APP_KEY injected from secret (MINIO_ROOT_PASSWORD)
|
||||
f\"B2_BUCKET_NAME={val(st['bucket'])}\",
|
||||
'B2_ENDPOINT=minio.honeydue.svc.cluster.local:9000',
|
||||
'STORAGE_USE_SSL=false',
|
||||
f\"STORAGE_MAX_FILE_SIZE={st['max_file_size']}\",
|
||||
f\"STORAGE_ALLOWED_TYPES={st['allowed_types']}\",
|
||||
# MinIO root user (for MinIO deployment + bucket init job)
|
||||
f\"MINIO_ROOT_USER={val(st['minio_root_user'])}\",
|
||||
# Features
|
||||
f\"FEATURE_PUSH_ENABLED={b(ft['push_enabled'])}\",
|
||||
f\"FEATURE_EMAIL_ENABLED={b(ft['email_enabled'])}\",
|
||||
f\"FEATURE_WEBHOOKS_ENABLED={b(ft['webhooks_enabled'])}\",
|
||||
f\"FEATURE_ONBOARDING_EMAILS_ENABLED={b(ft['onboarding_emails_enabled'])}\",
|
||||
f\"FEATURE_PDF_REPORTS_ENABLED={b(ft['pdf_reports_enabled'])}\",
|
||||
f\"FEATURE_WORKER_ENABLED={b(ft['worker_enabled'])}\",
|
||||
# Apple auth/IAP
|
||||
f\"APPLE_CLIENT_ID={val(aa.get('client_id'))}\",
|
||||
f\"APPLE_TEAM_ID={val(aa.get('team_id'))}\",
|
||||
f\"APPLE_IAP_KEY_ID={val(aa.get('iap_key_id'))}\",
|
||||
f\"APPLE_IAP_ISSUER_ID={val(aa.get('iap_issuer_id'))}\",
|
||||
f\"APPLE_IAP_BUNDLE_ID={val(aa.get('iap_bundle_id'))}\",
|
||||
f\"APPLE_IAP_KEY_PATH={val(aa.get('iap_key_path'))}\",
|
||||
f\"APPLE_IAP_SANDBOX={b(aa.get('iap_sandbox', True))}\",
|
||||
# Google auth/IAP
|
||||
f\"GOOGLE_CLIENT_ID={val(ga.get('client_id'))}\",
|
||||
f\"GOOGLE_ANDROID_CLIENT_ID={val(ga.get('android_client_id'))}\",
|
||||
f\"GOOGLE_IOS_CLIENT_ID={val(ga.get('ios_client_id'))}\",
|
||||
f\"GOOGLE_IAP_PACKAGE_NAME={val(ga.get('iap_package_name'))}\",
|
||||
f\"GOOGLE_IAP_SERVICE_ACCOUNT_PATH={val(ga.get('iap_service_account_path'))}\",
|
||||
]
|
||||
|
||||
print('\n'.join(lines))
|
||||
"
|
||||
}
|
||||
61
deploy-k3s-dev/scripts/rollback.sh
Executable file
61
deploy-k3s-dev/scripts/rollback.sh
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
NAMESPACE="honeydue"
|
||||
|
||||
log() { printf '[rollback] %s\n' "$*"; }
|
||||
die() { printf '[rollback][error] %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
command -v kubectl >/dev/null 2>&1 || die "Missing: kubectl"
|
||||
|
||||
DEPLOYMENTS=("api" "worker" "admin")
|
||||
|
||||
# --- Show current state ---
|
||||
|
||||
echo "=== Current Rollout History ==="
|
||||
for deploy in "${DEPLOYMENTS[@]}"; do
|
||||
echo ""
|
||||
echo "--- ${deploy} ---"
|
||||
kubectl rollout history deployment/"${deploy}" -n "${NAMESPACE}" 2>/dev/null || echo " (not found)"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Current Images ==="
|
||||
for deploy in "${DEPLOYMENTS[@]}"; do
|
||||
IMAGE="$(kubectl get deployment "${deploy}" -n "${NAMESPACE}" -o jsonpath='{.spec.template.spec.containers[0].image}' 2>/dev/null || echo "n/a")"
|
||||
echo " ${deploy}: ${IMAGE}"
|
||||
done
|
||||
|
||||
# --- Confirm ---
|
||||
|
||||
echo ""
|
||||
read -rp "Roll back all deployments to previous revision? [y/N] " confirm
|
||||
if [[ "${confirm}" != "y" && "${confirm}" != "Y" ]]; then
|
||||
log "Aborted."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Rollback ---
|
||||
|
||||
for deploy in "${DEPLOYMENTS[@]}"; do
|
||||
log "Rolling back ${deploy}..."
|
||||
kubectl rollout undo deployment/"${deploy}" -n "${NAMESPACE}" 2>/dev/null || log "Skipping ${deploy} (not found or no previous revision)"
|
||||
done
|
||||
|
||||
# --- Wait ---
|
||||
|
||||
log "Waiting for rollouts..."
|
||||
for deploy in "${DEPLOYMENTS[@]}"; do
|
||||
kubectl rollout status deployment/"${deploy}" -n "${NAMESPACE}" --timeout=300s 2>/dev/null || true
|
||||
done
|
||||
|
||||
# --- Verify ---
|
||||
|
||||
echo ""
|
||||
echo "=== Post-Rollback Images ==="
|
||||
for deploy in "${DEPLOYMENTS[@]}"; do
|
||||
IMAGE="$(kubectl get deployment "${deploy}" -n "${NAMESPACE}" -o jsonpath='{.spec.template.spec.containers[0].image}' 2>/dev/null || echo "n/a")"
|
||||
echo " ${deploy}: ${IMAGE}"
|
||||
done
|
||||
|
||||
log "Rollback complete. Run ./scripts/04-verify.sh to check health."
|
||||
22
deploy-k3s-dev/secrets/README.md
Normal file
22
deploy-k3s-dev/secrets/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Secrets Directory
|
||||
|
||||
Create these files before running `scripts/02-setup-secrets.sh`:
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `postgres_password.txt` | In-cluster PostgreSQL password |
|
||||
| `secret_key.txt` | App signing secret (minimum 32 characters) |
|
||||
| `email_host_password.txt` | SMTP password (Fastmail app password) |
|
||||
| `fcm_server_key.txt` | Firebase Cloud Messaging server key (optional — Android not yet ready) |
|
||||
| `apns_auth_key.p8` | Apple Push Notification private key |
|
||||
| `minio_root_password.txt` | MinIO root password (minimum 8 characters) |
|
||||
|
||||
Optional (only if `tls.mode: cloudflare` in config.yaml):
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `cloudflare-origin.crt` | Cloudflare origin certificate (PEM) |
|
||||
| `cloudflare-origin.key` | Cloudflare origin certificate key (PEM) |
|
||||
|
||||
All string config (registry token, domains, etc.) goes in `config.yaml` instead.
|
||||
These files are gitignored and should never be committed.
|
||||
Reference in New Issue
Block a user