Compare commits
1 Commits
326cca5870
...
mailu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b0c56f720 |
@@ -369,30 +369,6 @@ When apps need database URLs with embedded credentials, **always use a dedicated
|
||||
|
||||
Add `apps.myapp.dbUrl` to your manifest's `defaultSecrets`, and the system will generate the complete URL with embedded credentials automatically when the app is added.
|
||||
|
||||
### Backup/Restore Database Name Conventions
|
||||
|
||||
Wild Cloud's backup/restore system uses blue-green deployments. During restore, a standby copy of the app is created with a colored database name (e.g., `myapp_green`). The system automatically patches env vars in your Kubernetes resources to point to the standby database.
|
||||
|
||||
**How it works:** The restore system compiles your kustomize resources, finds env vars whose values match the original database name, and generates kustomize JSON patches to replace them with the standby database name. It uses env var naming conventions to distinguish database name fields from username fields (since both often have the same value).
|
||||
|
||||
**Env var naming guidelines for database-related fields:**
|
||||
|
||||
- **Database name env vars** should contain one of: `DATABASE`, `DB_NAME`, `DBNAME`, or `__DATABASE` in the env var name (e.g., `LISTMONK_db__database`, `DB_NAME`, `POSTGRES_DB`)
|
||||
- **Database URL env vars** are detected by containing `://` in the value (e.g., `postgresql://user:pass@host/dbname`)
|
||||
- **Username env vars** should contain `USER` in the name (e.g., `DB_USER`, `LISTMONK_db__user`) — these will NOT be patched even if the value matches the database name
|
||||
- Avoid env var names that are ambiguous about whether they hold a database name or username
|
||||
|
||||
**Example — correct naming:**
|
||||
```yaml
|
||||
env:
|
||||
- name: DB_NAME # Will be patched (contains "DB_NAME")
|
||||
value: myapp
|
||||
- name: DB_USER # Will NOT be patched (contains "USER")
|
||||
value: myapp
|
||||
- name: DATABASE_URL # Will be patched (contains "://")
|
||||
value: "postgresql://myapp:secret@postgres/myapp"
|
||||
```
|
||||
|
||||
## Security Requirements
|
||||
|
||||
### Security Contexts
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,233 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
if [ -z "${WILD_INSTANCE}" ]; then
|
||||
echo "ERROR: WILD_INSTANCE is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${WILD_API_DATA_DIR}" ]; then
|
||||
echo "ERROR: WILD_API_DATA_DIR is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${KUBECONFIG}" ]; then
|
||||
echo "ERROR: KUBECONFIG is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INSTANCE_DIR="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}"
|
||||
CERT_MANAGER_DIR="${INSTANCE_DIR}/apps/cert-manager"
|
||||
|
||||
echo "=== Setting up cert-manager ==="
|
||||
echo ""
|
||||
|
||||
#######################
|
||||
# Dependencies
|
||||
#######################
|
||||
|
||||
echo "Verifying Traefik is ready (required for cert-manager)..."
|
||||
kubectl wait --for=condition=Available deployment/traefik -n traefik --timeout=60s 2>/dev/null || {
|
||||
echo "WARNING: Traefik not ready, but continuing with cert-manager installation"
|
||||
echo "Note: cert-manager may not work properly without Traefik"
|
||||
}
|
||||
|
||||
if [ ! -f "${CERT_MANAGER_DIR}/kustomization.yaml" ]; then
|
||||
echo "ERROR: Compiled templates not found at ${CERT_MANAGER_DIR}/"
|
||||
echo "Templates should be compiled before deployment."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
########################
|
||||
# Kubernetes components
|
||||
########################
|
||||
|
||||
echo "Installing cert-manager components..."
|
||||
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.17.2/cert-manager.yaml || \
|
||||
kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.17.2/cert-manager.yaml
|
||||
|
||||
echo "Waiting for cert-manager to be ready..."
|
||||
kubectl wait --for=condition=Available deployment/cert-manager -n cert-manager --timeout=120s
|
||||
kubectl wait --for=condition=Available deployment/cert-manager-cainjector -n cert-manager --timeout=120s
|
||||
kubectl wait --for=condition=Available deployment/cert-manager-webhook -n cert-manager --timeout=120s
|
||||
|
||||
echo "Creating Cloudflare API token secret..."
|
||||
SECRETS_FILE="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}/secrets.yaml"
|
||||
CLOUDFLARE_API_TOKEN=$(yq '.apps.cert-manager.cloudflareToken' "$SECRETS_FILE" 2>/dev/null)
|
||||
|
||||
CLOUDFLARE_API_TOKEN=$(echo "$CLOUDFLARE_API_TOKEN")
|
||||
if [ -z "$CLOUDFLARE_API_TOKEN" ] || [ "$CLOUDFLARE_API_TOKEN" = "null" ]; then
|
||||
echo "ERROR: Cloudflare API token not found"
|
||||
echo "Please set: apps.cert-manager.cloudflareToken in secrets.yaml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
kubectl create secret generic cloudflare-api-token \
|
||||
--namespace cert-manager \
|
||||
--from-literal=api-token="${CLOUDFLARE_API_TOKEN}" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
echo "Verifying cert-manager webhook is fully operational..."
|
||||
until kubectl get validatingwebhookconfigurations cert-manager-webhook &>/dev/null; do
|
||||
echo "Waiting for cert-manager webhook to register..."
|
||||
sleep 5
|
||||
done
|
||||
|
||||
echo "Configuring cert-manager to use external DNS servers..."
|
||||
kubectl patch deployment cert-manager -n cert-manager --patch '
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
dnsPolicy: None
|
||||
dnsConfig:
|
||||
nameservers:
|
||||
- "1.1.1.1"
|
||||
- "8.8.8.8"
|
||||
searches:
|
||||
- cert-manager.svc.cluster.local
|
||||
- svc.cluster.local
|
||||
- cluster.local
|
||||
options:
|
||||
- name: ndots
|
||||
value: "5"'
|
||||
|
||||
echo "Waiting for cert-manager to restart with new DNS configuration..."
|
||||
kubectl rollout status deployment/cert-manager -n cert-manager --timeout=120s
|
||||
|
||||
########################
|
||||
# Create issuers and certificates
|
||||
########################
|
||||
|
||||
echo "Creating Let's Encrypt issuers and certificates..."
|
||||
kubectl apply -k ${CERT_MANAGER_DIR}/
|
||||
|
||||
echo "Waiting for Let's Encrypt issuers to be ready..."
|
||||
kubectl wait --for=condition=Ready clusterissuer/letsencrypt-prod --timeout=60s || echo "WARNING: Production issuer not ready, proceeding anyway..."
|
||||
kubectl wait --for=condition=Ready clusterissuer/letsencrypt-staging --timeout=60s || echo "WARNING: Staging issuer not ready, proceeding anyway..."
|
||||
|
||||
sleep 5
|
||||
|
||||
######################################
|
||||
# Fix stuck certificates and cleanup
|
||||
######################################
|
||||
|
||||
needs_restart=false
|
||||
|
||||
echo "Checking for certificates with failed issuance attempts..."
|
||||
stuck_certs=$(kubectl get certificates --all-namespaces -o json 2>/dev/null | \
|
||||
jq -r '.items[] | select(.status.conditions[]? | select(.type=="Issuing" and .status=="False" and (.message | contains("404")))) | "\(.metadata.namespace) \(.metadata.name)"')
|
||||
|
||||
if [ -n "$stuck_certs" ]; then
|
||||
echo "WARNING: Found certificates stuck with non-existent orders, recreating them..."
|
||||
echo "$stuck_certs" | while read ns name; do
|
||||
echo "Recreating certificate $ns/$name..."
|
||||
cert_spec=$(kubectl get certificate "$name" -n "$ns" -o json | jq '.spec')
|
||||
kubectl delete certificate "$name" -n "$ns"
|
||||
echo "{\"apiVersion\":\"cert-manager.io/v1\",\"kind\":\"Certificate\",\"metadata\":{\"name\":\"$name\",\"namespace\":\"$ns\"},\"spec\":$cert_spec}" | kubectl apply -f -
|
||||
done
|
||||
needs_restart=true
|
||||
sleep 5
|
||||
else
|
||||
echo "No certificates stuck with failed orders"
|
||||
fi
|
||||
|
||||
echo "Checking for orphaned ACME orders..."
|
||||
orphaned_orders=$(kubectl logs -n cert-manager deployment/cert-manager --tail=200 2>/dev/null | \
|
||||
grep -E "failed to retrieve the ACME order.*404" 2>/dev/null | \
|
||||
sed -n 's/.*resource_name="\([^"]*\)".*/\1/p' | \
|
||||
sort -u || true)
|
||||
|
||||
if [ -n "$orphaned_orders" ]; then
|
||||
echo "WARNING: Found orphaned ACME orders from logs"
|
||||
for order in $orphaned_orders; do
|
||||
echo "Deleting orphaned order: $order"
|
||||
orders_found=$(kubectl get orders --all-namespaces 2>/dev/null | grep "$order" 2>/dev/null || true)
|
||||
if [ -n "$orders_found" ]; then
|
||||
echo "$orders_found" | while read ns name rest; do
|
||||
kubectl delete order "$name" -n "$ns" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
done
|
||||
needs_restart=true
|
||||
else
|
||||
echo "No orphaned orders found in logs"
|
||||
fi
|
||||
|
||||
echo "Checking for Cloudflare DNS cleanup errors..."
|
||||
cloudflare_errors=$(kubectl logs -n cert-manager deployment/cert-manager --tail=200 2>/dev/null | \
|
||||
grep -c "Error: 7003.*Could not route" 2>/dev/null || echo "0")
|
||||
|
||||
if [ "$cloudflare_errors" -gt "0" ]; then
|
||||
echo "WARNING: Found $cloudflare_errors Cloudflare DNS cleanup errors (stale DNS record references)"
|
||||
echo "Deleting stuck challenges and orders to allow fresh start"
|
||||
|
||||
kubectl delete challenges --all -n cert-manager 2>/dev/null || true
|
||||
kubectl delete orders --all -n cert-manager 2>/dev/null || true
|
||||
|
||||
needs_restart=true
|
||||
else
|
||||
echo "No Cloudflare DNS cleanup errors"
|
||||
fi
|
||||
|
||||
if [ "$needs_restart" = true ]; then
|
||||
echo "Restarting cert-manager to clear internal state..."
|
||||
kubectl rollout restart deployment cert-manager -n cert-manager
|
||||
kubectl rollout status deployment/cert-manager -n cert-manager --timeout=120s
|
||||
echo "Waiting for cert-manager to recreate fresh challenges..."
|
||||
sleep 15
|
||||
else
|
||||
echo "No restart needed - cert-manager state is clean"
|
||||
fi
|
||||
|
||||
#########################
|
||||
# Final checks
|
||||
#########################
|
||||
|
||||
echo "Waiting for wildcard certificates to be ready (this may take several minutes)..."
|
||||
|
||||
wait_for_cert() {
|
||||
local cert_name="$1"
|
||||
local timeout=300
|
||||
local elapsed=0
|
||||
|
||||
echo " Checking $cert_name..."
|
||||
|
||||
while [ $elapsed -lt $timeout ]; do
|
||||
if kubectl get certificate "$cert_name" -n cert-manager -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null | grep -q "True"; then
|
||||
echo " $cert_name is ready"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ $((elapsed % 30)) -eq 0 ] && [ $elapsed -gt 0 ]; then
|
||||
local status=$(kubectl get certificate "$cert_name" -n cert-manager -o jsonpath='{.status.conditions[?(@.type=="Ready")].message}' 2>/dev/null || echo "Waiting...")
|
||||
echo " Still waiting for $cert_name... ($elapsed/${timeout}s) - $status"
|
||||
fi
|
||||
|
||||
sleep 5
|
||||
elapsed=$((elapsed + 5))
|
||||
done
|
||||
|
||||
echo " WARNING: Timeout waiting for $cert_name (will continue anyway)"
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_cert "wildcard-internal-wild-cloud"
|
||||
wait_for_cert "wildcard-wild-cloud"
|
||||
|
||||
echo "Performing final cert-manager health check..."
|
||||
failed_certs=$(kubectl get certificates --all-namespaces -o json 2>/dev/null | jq -r '.items[] | select(.status.conditions[]? | select(.type=="Ready" and .status!="True")) | "\(.metadata.namespace)/\(.metadata.name)"' | wc -l)
|
||||
if [ "$failed_certs" -gt 0 ]; then
|
||||
echo "WARNING: Found $failed_certs certificates not in Ready state"
|
||||
echo "Check certificate status with: kubectl get certificates --all-namespaces"
|
||||
echo "Check cert-manager logs with: kubectl logs -n cert-manager deployment/cert-manager"
|
||||
else
|
||||
echo "All certificates are in Ready state"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "cert-manager setup complete!"
|
||||
echo ""
|
||||
echo "To verify the installation:"
|
||||
echo " kubectl get certificates --all-namespaces"
|
||||
echo " kubectl get clusterissuers"
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: wildcard-internal-wild-cloud
|
||||
namespace: cert-manager
|
||||
spec:
|
||||
secretName: wildcard-internal-wild-cloud-tls
|
||||
dnsNames:
|
||||
- "*.{{ .internalDomain }}"
|
||||
- "{{ .internalDomain }}"
|
||||
issuerRef:
|
||||
name: letsencrypt-prod
|
||||
kind: ClusterIssuer
|
||||
duration: 2160h # 90 days
|
||||
renewBefore: 360h # 15 days
|
||||
privateKey:
|
||||
algorithm: RSA
|
||||
size: 2048
|
||||
@@ -1,9 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- letsencrypt-staging-dns01.yaml
|
||||
- letsencrypt-prod-dns01.yaml
|
||||
- internal-wildcard-certificate.yaml
|
||||
- wildcard-certificate.yaml
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-prod
|
||||
spec:
|
||||
acme:
|
||||
email: {{ .email }}
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-prod
|
||||
server: https://acme-v02.api.letsencrypt.org/directory
|
||||
solvers:
|
||||
# DNS-01 solver for wildcard certificates
|
||||
- dns01:
|
||||
cloudflare:
|
||||
apiTokenSecretRef:
|
||||
name: cloudflare-api-token
|
||||
key: api-token
|
||||
selector:
|
||||
dnsZones:
|
||||
- "{{ .cloudflareDomain }}"
|
||||
# Keep the HTTP-01 solver for non-wildcard certificates
|
||||
- http01:
|
||||
ingress:
|
||||
class: traefik
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-staging
|
||||
spec:
|
||||
acme:
|
||||
email: {{ .email }}
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-staging
|
||||
server: https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
solvers:
|
||||
# DNS-01 solver for wildcard certificates
|
||||
- dns01:
|
||||
cloudflare:
|
||||
apiTokenSecretRef:
|
||||
name: cloudflare-api-token
|
||||
key: api-token
|
||||
selector:
|
||||
dnsZones:
|
||||
- "{{ .cloudflareDomain }}"
|
||||
# Keep the HTTP-01 solver for non-wildcard certificates
|
||||
- http01:
|
||||
ingress:
|
||||
class: traefik
|
||||
@@ -1,15 +0,0 @@
|
||||
name: cert-manager
|
||||
is: cert-manager
|
||||
description: X.509 certificate management for Kubernetes
|
||||
version: v1.17.2
|
||||
namespace: cert-manager
|
||||
category: infrastructure
|
||||
requires:
|
||||
- name: traefik
|
||||
defaultConfig:
|
||||
cloudDomain: "{{ .cloud.domain }}"
|
||||
internalDomain: "{{ .cloud.internalDomain }}"
|
||||
email: "{{ .operator.email }}"
|
||||
cloudflareDomain: "{{ .cloud.baseDomain }}"
|
||||
defaultSecrets:
|
||||
- key: cloudflareToken
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: wildcard-wild-cloud
|
||||
namespace: cert-manager
|
||||
spec:
|
||||
secretName: wildcard-wild-cloud-tls
|
||||
dnsNames:
|
||||
- "*.{{ .cloudDomain }}"
|
||||
- "{{ .cloudDomain }}"
|
||||
issuerRef:
|
||||
name: letsencrypt-prod
|
||||
kind: ClusterIssuer
|
||||
duration: 2160h # 90 days
|
||||
renewBefore: 360h # 15 days
|
||||
privateKey:
|
||||
algorithm: RSA
|
||||
size: 2048
|
||||
@@ -1,75 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: communitarian-api
|
||||
namespace: "{{ .namespace }}"
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
component: api
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
component: api
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
fsGroup: 1000
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
initContainers:
|
||||
- name: fix-permissions
|
||||
image: busybox:1.36
|
||||
command: ['sh', '-c']
|
||||
args:
|
||||
- |
|
||||
mkdir -p /app/api/data/citizens /app/api/data/communities /app/api/data/content /app/api/data/memberships /app/api/data/reactions
|
||||
chmod -R 777 /app/api/data
|
||||
echo "Permissions fixed"
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /app/api/data
|
||||
securityContext:
|
||||
runAsUser: 0
|
||||
runAsNonRoot: false
|
||||
containers:
|
||||
- name: communitarian-api
|
||||
image: "{{ .apiImage }}"
|
||||
ports:
|
||||
- containerPort: {{ .apiPort }}
|
||||
name: http
|
||||
env:
|
||||
- name: TZ
|
||||
value: "{{ .timezone }}"
|
||||
- name: API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: communitarian-secrets
|
||||
key: apiKey
|
||||
- name: JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: communitarian-secrets
|
||||
key: jwtSecret
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /app/api/data
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "500m"
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: [ALL]
|
||||
readOnlyRootFilesystem: false
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: communitarian-data
|
||||
@@ -1,47 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: communitarian-app
|
||||
namespace: "{{ .namespace }}"
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
component: app
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
component: app
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
fsGroup: 1000
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: communitarian-app
|
||||
image: "{{ .appImage }}"
|
||||
ports:
|
||||
- containerPort: {{ .appPort }}
|
||||
name: http
|
||||
env:
|
||||
- name: TZ
|
||||
value: "{{ .timezone }}"
|
||||
- name: API_URL
|
||||
value: "http://communitarian-api:{{ .apiPort }}"
|
||||
- name: NEXT_PUBLIC_API_URL
|
||||
value: "/api"
|
||||
resources:
|
||||
requests:
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: [ALL]
|
||||
readOnlyRootFilesystem: false
|
||||
@@ -1,33 +0,0 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: communitarian
|
||||
namespace: "{{ .namespace }}"
|
||||
annotations:
|
||||
external-dns.alpha.kubernetes.io/target: "{{ .externalDnsDomain }}"
|
||||
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
|
||||
traefik.ingress.kubernetes.io/router.middlewares: "{{ .namespace }}-strip-api@kubernetescrd"
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
tls:
|
||||
- hosts:
|
||||
- "{{ .domain }}"
|
||||
secretName: "{{ .tlsSecretName }}"
|
||||
rules:
|
||||
- host: "{{ .domain }}"
|
||||
http:
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: communitarian-api
|
||||
port:
|
||||
number: {{ .apiPort }}
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: communitarian-app
|
||||
port:
|
||||
number: {{ .appPort }}
|
||||
@@ -1,18 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: "{{ .namespace }}"
|
||||
labels:
|
||||
- includeSelectors: true
|
||||
pairs:
|
||||
app: communitarian
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- deployment-app.yaml
|
||||
- deployment-api.yaml
|
||||
- service-app.yaml
|
||||
- service-api.yaml
|
||||
- middleware.yaml
|
||||
- ingress.yaml
|
||||
- pvc.yaml
|
||||
@@ -1,19 +0,0 @@
|
||||
name: communitarian
|
||||
is: communitarian
|
||||
description: Communitarian is a community-focused application with a web frontend and API backend for collaborative features.
|
||||
version: 1.0.0
|
||||
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/community.svg
|
||||
defaultConfig:
|
||||
namespace: communitarian
|
||||
appImage: payneio/communitarian-app:latest
|
||||
apiImage: payneio/communitarian-api:latest
|
||||
appPort: 3000
|
||||
apiPort: 8000
|
||||
domain: communitarian.{{ .cloud.domain }}
|
||||
externalDnsDomain: "{{ .cloud.domain }}"
|
||||
tlsSecretName: wildcard-wild-cloud-tls
|
||||
storage: 10Gi
|
||||
timezone: UTC
|
||||
defaultSecrets:
|
||||
- key: apiKey
|
||||
- key: jwtSecret
|
||||
@@ -1,9 +0,0 @@
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: strip-api
|
||||
namespace: "{{ .namespace }}"
|
||||
spec:
|
||||
stripPrefix:
|
||||
prefixes:
|
||||
- /api
|
||||
@@ -1,4 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: "{{ .namespace }}"
|
||||
@@ -1,11 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: communitarian-data
|
||||
namespace: "{{ .namespace }}"
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: "{{ .storage }}"
|
||||
@@ -1,13 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: communitarian-api
|
||||
namespace: "{{ .namespace }}"
|
||||
spec:
|
||||
selector:
|
||||
component: api
|
||||
ports:
|
||||
- port: {{ .apiPort }}
|
||||
targetPort: {{ .apiPort }}
|
||||
protocol: TCP
|
||||
name: http
|
||||
@@ -1,13 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: communitarian-app
|
||||
namespace: "{{ .namespace }}"
|
||||
spec:
|
||||
selector:
|
||||
component: app
|
||||
ports:
|
||||
- port: {{ .appPort }}
|
||||
targetPort: {{ .appPort }}
|
||||
protocol: TCP
|
||||
name: http
|
||||
@@ -1,45 +0,0 @@
|
||||
# CoreDNS
|
||||
|
||||
- https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/
|
||||
- https://github.com/kubernetes/dns/blob/master/docs/specification.md
|
||||
- https://coredns.io/
|
||||
|
||||
CoreDNS has the `kubernetes` plugin, so it returns all k8s service endpoints in well-known format.
|
||||
|
||||
All services and pods are registered in CoreDNS.
|
||||
|
||||
- <service-name>.<namespace>.svc.cluster.local
|
||||
- <service-name>.<namespace>
|
||||
- <service-name> (if in the same namespace)
|
||||
|
||||
- <pod-ipv4-address>.<namespace>.pod.cluster.local
|
||||
- <pod-ipv4-address>.<service-name>.<namespace>.svc.cluster.local
|
||||
|
||||
Any query for a resource in the `internal.$DOMAIN` domain will be given the IP of the Traefik proxy. We expose the CoreDNS server in the LAN via MetalLB just for this capability.
|
||||
|
||||
## Default CoreDNS Configuration
|
||||
|
||||
This is the default CoreDNS configuration, for reference:
|
||||
|
||||
```txt
|
||||
.:53 {
|
||||
errors
|
||||
health { lameduck 5s }
|
||||
ready
|
||||
log . { class error }
|
||||
prometheus :9153
|
||||
kubernetes cluster.local in-addr.arpa ip6.arpa {
|
||||
pods insecure
|
||||
fallthrough in-addr.arpa ip6.arpa
|
||||
ttl 30
|
||||
}
|
||||
forward . /etc/resolv.conf { max_concurrent 1000 }
|
||||
cache 30 {
|
||||
disable success cluster.local
|
||||
disable denial cluster.local
|
||||
}
|
||||
loop
|
||||
reload
|
||||
loadbalance
|
||||
}
|
||||
```
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: coredns-custom
|
||||
namespace: kube-system
|
||||
data:
|
||||
# Custom server block for internal domains. All internal domains should
|
||||
# resolve to the cluster proxy.
|
||||
internal.server: |
|
||||
{{ .internalDomain }} {
|
||||
errors
|
||||
cache 30
|
||||
reload
|
||||
template IN A {
|
||||
match (.*)\.{{ .internalDomain | strings.ReplaceAll "." "\\." }}\.
|
||||
answer "{{`{{ .Name }}`}} 60 IN A {{ .loadBalancerIp }}"
|
||||
}
|
||||
template IN AAAA {
|
||||
match (.*)\.{{ .internalDomain | strings.ReplaceAll "." "\\." }}\.
|
||||
rcode NXDOMAIN
|
||||
}
|
||||
}
|
||||
# Custom override to set external resolvers.
|
||||
external.override: |
|
||||
forward . {{ .externalResolver }} {
|
||||
max_concurrent 1000
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
if [ -z "${WILD_INSTANCE}" ]; then
|
||||
echo "ERROR: WILD_INSTANCE is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${WILD_API_DATA_DIR}" ]; then
|
||||
echo "ERROR: WILD_API_DATA_DIR is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${KUBECONFIG}" ]; then
|
||||
echo "ERROR: KUBECONFIG is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INSTANCE_DIR="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}"
|
||||
COREDNS_DIR="${INSTANCE_DIR}/apps/coredns"
|
||||
|
||||
echo "=== Setting up CoreDNS ==="
|
||||
echo ""
|
||||
|
||||
echo "Using pre-compiled CoreDNS templates..."
|
||||
if [ ! -f "${COREDNS_DIR}/kustomization.yaml" ]; then
|
||||
echo "ERROR: Compiled templates not found at ${COREDNS_DIR}"
|
||||
echo "Templates should be compiled before deployment."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Applying CoreDNS custom override configuration..."
|
||||
kubectl apply -k "${COREDNS_DIR}/"
|
||||
|
||||
echo "Restarting CoreDNS pods to apply changes..."
|
||||
kubectl rollout restart deployment/coredns -n kube-system
|
||||
echo "Waiting for CoreDNS rollout to complete..."
|
||||
kubectl rollout status deployment/coredns -n kube-system
|
||||
|
||||
echo ""
|
||||
echo "CoreDNS configured successfully"
|
||||
echo ""
|
||||
echo "To verify the installation:"
|
||||
echo " kubectl get pods -n kube-system -l k8s-app=kube-dns"
|
||||
echo " kubectl get svc -n kube-system coredns"
|
||||
echo " kubectl describe svc -n kube-system coredns"
|
||||
echo ""
|
||||
echo "To view CoreDNS logs:"
|
||||
echo " kubectl logs -n kube-system -l k8s-app=kube-dns -f"
|
||||
@@ -1,5 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- coredns-custom-config.yaml
|
||||
@@ -1,12 +0,0 @@
|
||||
name: coredns
|
||||
is: coredns
|
||||
description: DNS server for internal cluster DNS resolution
|
||||
version: v1.12.0
|
||||
namespace: kube-system
|
||||
category: infrastructure
|
||||
requires:
|
||||
- name: metallb
|
||||
defaultConfig:
|
||||
internalDomain: "{{ .cloud.internalDomain }}"
|
||||
loadBalancerIp: "{{ .apps.metallb.loadBalancerIp }}"
|
||||
externalResolver: "8.8.8.8"
|
||||
@@ -1,118 +0,0 @@
|
||||
# CrowdSec Security Service
|
||||
|
||||
CrowdSec is an open-source security engine that analyzes traffic patterns and blocks malicious actors. This service integrates CrowdSec with Traefik to provide automatic threat detection and rate limiting for all Wild Cloud ingresses.
|
||||
|
||||
## Components
|
||||
|
||||
- **CrowdSec Agent**: Analyzes traffic patterns, maintains decision lists, and connects to the CrowdSec threat intelligence network
|
||||
- **Traefik Bouncer**: Integrates with Traefik via ForwardAuth to enforce CrowdSec decisions
|
||||
- **Security Middlewares**: Traefik middleware for rate limiting and security headers
|
||||
|
||||
## Default Protection
|
||||
|
||||
After installation, **all ingresses are automatically protected** with:
|
||||
- Threat detection (blocks known malicious IPs and attack patterns)
|
||||
- Rate limiting (100 requests per minute per IP)
|
||||
- Security headers (HSTS, XSS protection, content-type sniffing prevention)
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is stored in `config.yaml` under `apps.crowdsec`:
|
||||
|
||||
```yaml
|
||||
apps:
|
||||
crowdsec:
|
||||
rateLimitAverage: "100"
|
||||
rateLimitBurst: "100"
|
||||
```
|
||||
|
||||
## Secrets
|
||||
|
||||
Secrets are stored in `secrets.yaml` under `apps.crowdsec`:
|
||||
|
||||
```yaml
|
||||
apps:
|
||||
crowdsec:
|
||||
agentPassword: <auto-generated>
|
||||
bouncerApiKey: <auto-generated>
|
||||
```
|
||||
|
||||
## Opting Out
|
||||
|
||||
To disable CrowdSec protection for a specific ingress (e.g., webhooks, health checks):
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.middlewares: ""
|
||||
```
|
||||
|
||||
## Using Only Rate Limiting
|
||||
|
||||
To use rate limiting without threat detection:
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.middlewares: crowdsec-rate-limit@kubernetescrd
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
View active decisions (blocked IPs):
|
||||
```bash
|
||||
kubectl exec -n crowdsec deploy/crowdsec -- cscli decisions list
|
||||
```
|
||||
|
||||
View registered bouncers:
|
||||
```bash
|
||||
kubectl exec -n crowdsec deploy/crowdsec -- cscli bouncers list
|
||||
```
|
||||
|
||||
View alerts:
|
||||
```bash
|
||||
kubectl exec -n crowdsec deploy/crowdsec -- cscli alerts list
|
||||
```
|
||||
|
||||
View metrics (Prometheus format):
|
||||
```bash
|
||||
kubectl port-forward -n crowdsec svc/crowdsec-lapi 6060:6060
|
||||
curl http://localhost:6060/metrics
|
||||
```
|
||||
|
||||
## Threat Intelligence
|
||||
|
||||
CrowdSec includes these detection collections:
|
||||
- `crowdsecurity/traefik` - Traefik-specific detections
|
||||
- `crowdsecurity/http-cve` - Known HTTP CVE exploits
|
||||
- `crowdsecurity/whitelist-good-actors` - Whitelist for known good actors (search engines, etc.)
|
||||
|
||||
Enabled scenarios:
|
||||
- HTTP probing and path traversal detection
|
||||
- Bad user agent detection
|
||||
- Sensitive file access attempts
|
||||
- HTTP crawling detection
|
||||
- SSH brute force (if exposed)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Bouncer not connecting to agent:**
|
||||
```bash
|
||||
kubectl logs -n crowdsec deploy/traefik-crowdsec-bouncer
|
||||
kubectl exec -n crowdsec deploy/crowdsec -- cscli bouncers list
|
||||
```
|
||||
|
||||
**Check if middleware is applied:**
|
||||
```bash
|
||||
kubectl get middleware -n crowdsec
|
||||
kubectl describe ingressroute -n <app-namespace> <route-name>
|
||||
```
|
||||
|
||||
**View CrowdSec logs:**
|
||||
```bash
|
||||
kubectl logs -n crowdsec deploy/crowdsec
|
||||
```
|
||||
@@ -1,43 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: crowdsec-config
|
||||
namespace: crowdsec
|
||||
labels:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
data:
|
||||
acquis.yaml: |
|
||||
filenames:
|
||||
- /var/log/containers/traefik-*_traefik_*.log
|
||||
force_inotify: true
|
||||
poll_without_inotify: true
|
||||
labels:
|
||||
type: containerd
|
||||
program: traefik
|
||||
profiles.yaml: |
|
||||
name: default_ip_remediation
|
||||
debug: false
|
||||
filters:
|
||||
- Alert.Remediation == true && Alert.GetScope() == "Ip"
|
||||
decisions:
|
||||
- type: ban
|
||||
duration: 4h
|
||||
on_success: break
|
||||
---
|
||||
name: default_range_remediation
|
||||
debug: false
|
||||
filters:
|
||||
- Alert.Remediation == true && Alert.GetScope() == "Range"
|
||||
decisions:
|
||||
- type: ban
|
||||
duration: 4h
|
||||
scope: Range
|
||||
on_success: break
|
||||
postoverflows.yaml: |
|
||||
# Post-overflow configuration for crowdsec
|
||||
name: "rdns"
|
||||
debug: false
|
||||
filter: "evt.Enriched.IsoCode != ''"
|
||||
# Add reverse DNS enrichment
|
||||
@@ -1,126 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: crowdsec
|
||||
namespace: crowdsec
|
||||
labels:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
spec:
|
||||
serviceAccountName: crowdsec
|
||||
affinity:
|
||||
podAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
podAffinityTerm:
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app: traefik
|
||||
topologyKey: kubernetes.io/hostname
|
||||
securityContext:
|
||||
runAsUser: 0
|
||||
runAsNonRoot: false
|
||||
fsGroup: 0
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: crowdsec
|
||||
image: crowdsecurity/crowdsec:v1.7.8
|
||||
env:
|
||||
- name: COLLECTIONS
|
||||
value: "crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/whitelist-good-actors crowdsecurity/iptables crowdsecurity/linux"
|
||||
- name: PARSERS
|
||||
value: "crowdsecurity/traefik-logs crowdsecurity/http-logs crowdsecurity/nginx-logs"
|
||||
- name: SCENARIOS
|
||||
value: "crowdsecurity/http-crawl-non_statics crowdsecurity/http-probing crowdsecurity/http-sensitive-files crowdsecurity/http-bad-user-agent crowdsecurity/http-path-traversal-probing crowdsecurity/ssh-bf crowdsecurity/ssh-slow-bf"
|
||||
- name: POSTOVERFLOWS
|
||||
value: "crowdsecurity/rdns crowdsecurity/cdn-whitelist"
|
||||
- name: GID
|
||||
value: "1000"
|
||||
- name: LEVEL_TRACE
|
||||
value: "false"
|
||||
- name: LEVEL_DEBUG
|
||||
value: "false"
|
||||
- name: LEVEL_INFO
|
||||
value: "true"
|
||||
- name: AGENT_USERNAME
|
||||
value: "kubernetes-cluster"
|
||||
- name: AGENT_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: crowdsec-agent-secret
|
||||
key: password
|
||||
ports:
|
||||
- name: lapi
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
- name: prometheus
|
||||
containerPort: 6060
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 200Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
runAsNonRoot: false
|
||||
volumeMounts:
|
||||
- name: crowdsec-config
|
||||
mountPath: /etc/crowdsec/acquis.yaml
|
||||
subPath: acquis.yaml
|
||||
readOnly: true
|
||||
- name: crowdsec-config
|
||||
mountPath: /etc/crowdsec/profiles.yaml
|
||||
subPath: profiles.yaml
|
||||
readOnly: true
|
||||
- name: crowdsec-data
|
||||
mountPath: /var/lib/crowdsec/data
|
||||
- name: crowdsec-config-dir
|
||||
mountPath: /etc/crowdsec/config
|
||||
- name: varlog
|
||||
mountPath: /var/log
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: crowdsec-config
|
||||
configMap:
|
||||
name: crowdsec-config
|
||||
- name: crowdsec-data
|
||||
persistentVolumeClaim:
|
||||
claimName: crowdsec-data
|
||||
- name: crowdsec-config-dir
|
||||
emptyDir: {}
|
||||
- name: varlog
|
||||
hostPath:
|
||||
path: /var/log
|
||||
@@ -1,24 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: crowdsec-lapi
|
||||
namespace: crowdsec
|
||||
labels:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
ports:
|
||||
- name: lapi
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
- name: prometheus
|
||||
port: 6060
|
||||
targetPort: 6060
|
||||
protocol: TCP
|
||||
@@ -1,118 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
if [ -z "${WILD_INSTANCE}" ]; then
|
||||
echo "ERROR: WILD_INSTANCE is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${WILD_API_DATA_DIR}" ]; then
|
||||
echo "ERROR: WILD_API_DATA_DIR is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${KUBECONFIG}" ]; then
|
||||
echo "ERROR: KUBECONFIG is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INSTANCE_DIR="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}"
|
||||
CROWDSEC_DIR="${INSTANCE_DIR}/apps/crowdsec"
|
||||
SECRETS_FILE="${INSTANCE_DIR}/secrets.yaml"
|
||||
|
||||
echo "=== Setting up CrowdSec Security Engine ==="
|
||||
echo ""
|
||||
|
||||
echo "Verifying Traefik is ready (required for CrowdSec bouncer)..."
|
||||
kubectl wait --for=condition=Available deployment/traefik -n traefik --timeout=60s 2>/dev/null || {
|
||||
echo "WARNING: Traefik not ready, but continuing with CrowdSec installation"
|
||||
echo "Note: CrowdSec bouncer will not work until Traefik is available"
|
||||
}
|
||||
|
||||
echo "Using pre-compiled CrowdSec templates..."
|
||||
if [ ! -f "${CROWDSEC_DIR}/kustomization.yaml" ]; then
|
||||
echo "ERROR: Compiled templates not found at ${CROWDSEC_DIR}"
|
||||
echo "Templates should be compiled before deployment."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Deploying CrowdSec..."
|
||||
kubectl apply -k ${CROWDSEC_DIR}/
|
||||
|
||||
echo "Creating CrowdSec agent secret..."
|
||||
AGENT_PASSWORD=$(yq '.apps.crowdsec.agentPassword' "$SECRETS_FILE" 2>/dev/null | tr -d '"')
|
||||
|
||||
if [ -z "$AGENT_PASSWORD" ] || [ "$AGENT_PASSWORD" = "null" ]; then
|
||||
echo "Generating new agent password..."
|
||||
AGENT_PASSWORD=$(openssl rand -base64 32)
|
||||
echo "WARNING: Agent password not found in secrets.yaml"
|
||||
echo "Using generated password - you may want to persist this"
|
||||
fi
|
||||
|
||||
kubectl create secret generic crowdsec-agent-secret \
|
||||
--namespace crowdsec \
|
||||
--from-literal=password="${AGENT_PASSWORD}" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
echo "Waiting for CrowdSec agent to be ready..."
|
||||
kubectl rollout status deployment/crowdsec -n crowdsec --timeout=120s
|
||||
|
||||
echo "Registering bouncer with CrowdSec agent..."
|
||||
BOUNCER_API_KEY=$(yq '.apps.crowdsec.bouncerApiKey' "$SECRETS_FILE" 2>/dev/null | tr -d '"')
|
||||
|
||||
if [ -z "$BOUNCER_API_KEY" ] || [ "$BOUNCER_API_KEY" = "null" ]; then
|
||||
echo "Generating new bouncer API key from CrowdSec agent..."
|
||||
kubectl exec -n crowdsec deploy/crowdsec -- cscli bouncers delete traefik-bouncer 2>/dev/null || true
|
||||
BOUNCER_API_KEY=$(kubectl exec -n crowdsec deploy/crowdsec -- cscli bouncers add traefik-bouncer -o raw)
|
||||
echo "Generated bouncer API key - you may want to persist this in secrets.yaml"
|
||||
fi
|
||||
|
||||
kubectl create secret generic crowdsec-bouncer-secret \
|
||||
--namespace crowdsec \
|
||||
--from-literal=api-key="${BOUNCER_API_KEY}" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
echo "Copying bouncer secret to traefik namespace..."
|
||||
kubectl create secret generic crowdsec-bouncer-secret \
|
||||
--namespace traefik \
|
||||
--from-literal=api-key="${BOUNCER_API_KEY}" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
echo "Cleaning up old bouncer deployment..."
|
||||
kubectl delete deployment traefik-crowdsec-bouncer -n crowdsec --ignore-not-found
|
||||
kubectl delete service traefik-crowdsec-bouncer -n crowdsec --ignore-not-found
|
||||
|
||||
echo "Restarting Traefik to load CrowdSec plugin..."
|
||||
kubectl rollout restart deployment/traefik -n traefik
|
||||
kubectl rollout status deployment/traefik -n traefik --timeout=120s
|
||||
|
||||
echo "Configuring Traefik to use CrowdSec security chain by default..."
|
||||
kubectl patch deployment traefik -n traefik --type='json' -p='[
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/spec/template/spec/containers/0/args/-",
|
||||
"value": "--entryPoints.websecure.http.middlewares=crowdsec-security-chain@kubernetescrd"
|
||||
}
|
||||
]' 2>/dev/null || {
|
||||
echo "Note: Traefik may already have middleware configured or patch failed"
|
||||
echo "You can manually configure default middleware if needed"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "CrowdSec installed successfully (using Traefik plugin)"
|
||||
echo ""
|
||||
echo "All ingresses are now protected by default with:"
|
||||
echo " - Threat detection (CrowdSec Traefik plugin, stream mode)"
|
||||
echo " - Rate limiting (100 req/min)"
|
||||
echo " - Security headers (HSTS, XSS protection, etc.)"
|
||||
echo ""
|
||||
echo "To verify the installation:"
|
||||
echo " kubectl get pods -n crowdsec"
|
||||
echo " kubectl get pods -n traefik"
|
||||
echo " kubectl exec -n crowdsec deploy/crowdsec -- cscli bouncers list"
|
||||
echo " kubectl exec -n crowdsec deploy/crowdsec -- cscli decisions list"
|
||||
echo ""
|
||||
echo "To opt-out a specific ingress from CrowdSec protection:"
|
||||
echo " Add annotation: traefik.ingress.kubernetes.io/router.middlewares: \"\""
|
||||
echo ""
|
||||
@@ -1,17 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: crowdsec
|
||||
labels:
|
||||
- includeSelectors: true
|
||||
pairs:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- serviceaccount.yaml
|
||||
- configmap.yaml
|
||||
- pvc.yaml
|
||||
- crowdsec-deployment.yaml
|
||||
- crowdsec-service.yaml
|
||||
- middleware.yaml
|
||||
@@ -1,15 +0,0 @@
|
||||
name: crowdsec
|
||||
is: crowdsec
|
||||
description: CrowdSec security engine with Traefik bouncer for threat detection and rate limiting
|
||||
version: v1.7.8
|
||||
namespace: crowdsec
|
||||
category: infrastructure
|
||||
requires:
|
||||
- name: longhorn
|
||||
- name: traefik
|
||||
defaultConfig:
|
||||
rateLimitAverage: "100"
|
||||
rateLimitBurst: "100"
|
||||
defaultSecrets:
|
||||
- key: agentPassword
|
||||
- key: bouncerApiKey
|
||||
@@ -1,89 +0,0 @@
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: crowdsec-bouncer
|
||||
namespace: crowdsec
|
||||
labels:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
spec:
|
||||
plugin:
|
||||
bouncer:
|
||||
crowdsecLapiScheme: http
|
||||
crowdsecLapiHost: crowdsec-lapi.crowdsec.svc.cluster.local:8080
|
||||
crowdsecLapiKeyFile: /etc/traefik/crowdsec/api-key
|
||||
crowdsecMode: stream
|
||||
updateIntervalSeconds: 15
|
||||
defaultDecisionSeconds: 60
|
||||
crowdsecAppsecEnabled: false
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: rate-limit
|
||||
namespace: crowdsec
|
||||
labels:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
spec:
|
||||
rateLimit:
|
||||
average: {{ .rateLimitAverage }}
|
||||
burst: {{ .rateLimitBurst }}
|
||||
period: 1m
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: security-headers
|
||||
namespace: crowdsec
|
||||
labels:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
spec:
|
||||
headers:
|
||||
browserXssFilter: true
|
||||
contentTypeNosniff: true
|
||||
forceSTSHeader: true
|
||||
frameDeny: true
|
||||
sslRedirect: true
|
||||
stsIncludeSubdomains: true
|
||||
stsPreload: true
|
||||
stsSeconds: 31536000
|
||||
addVaryHeader: true
|
||||
accessControlAllowMethods:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- DELETE
|
||||
- OPTIONS
|
||||
accessControlAllowOriginList:
|
||||
- "*"
|
||||
accessControlMaxAge: 100
|
||||
customRequestHeaders:
|
||||
X-Forwarded-Proto: https
|
||||
customResponseHeaders:
|
||||
Server: ""
|
||||
X-Robots-Tag: noindex,nofollow,nosnippet,noarchive,notranslate,noimageindex
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: security-chain
|
||||
namespace: crowdsec
|
||||
labels:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
spec:
|
||||
chain:
|
||||
middlewares:
|
||||
- name: security-headers
|
||||
namespace: crowdsec
|
||||
- name: rate-limit
|
||||
namespace: crowdsec
|
||||
- name: crowdsec-bouncer
|
||||
namespace: crowdsec
|
||||
@@ -1,9 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: crowdsec
|
||||
labels:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
pod-security.kubernetes.io/enforce: privileged
|
||||
@@ -1,12 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: crowdsec-data
|
||||
spec:
|
||||
storageClassName: longhorn
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
volumeMode: Filesystem
|
||||
resources:
|
||||
requests:
|
||||
storage: 512Mi
|
||||
@@ -1,9 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: crowdsec
|
||||
namespace: crowdsec
|
||||
labels:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
@@ -1,35 +0,0 @@
|
||||
# Decidim
|
||||
|
||||
Decidim is a participatory democracy framework for cities and organizations. Built in Ruby on Rails, it enables citizen participation through proposals, debates, and voting. Includes Sidekiq for background job processing.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **PostgreSQL** - Database for storing participatory processes and user data
|
||||
- **Redis** - Used for Sidekiq background job processing
|
||||
|
||||
## Configuration
|
||||
|
||||
Key settings configured through your instance's `config.yaml`:
|
||||
|
||||
- **domain** - Where Decidim will be accessible (default: `decidim.{your-cloud-domain}`)
|
||||
- **siteName** - Display name for your platform (default: `Decidim`)
|
||||
- **systemAdminEmail** - System admin email (defaults to your operator email)
|
||||
- **storage** - Persistent volume size (default: `20Gi`)
|
||||
- **SMTP** - Email delivery settings inherited from your Wild Cloud instance
|
||||
|
||||
## Access
|
||||
|
||||
After deployment, Decidim will be available at:
|
||||
- `https://decidim.{your-cloud-domain}`
|
||||
|
||||
## First-Time Setup
|
||||
|
||||
1. Add and deploy the app:
|
||||
```bash
|
||||
wild app add decidim
|
||||
wild app deploy decidim
|
||||
```
|
||||
|
||||
2. Log in with the system admin credentials configured during setup
|
||||
|
||||
3. Create your first organization and configure participatory processes
|
||||
@@ -8,7 +8,6 @@ requires:
|
||||
installed_as: postgres
|
||||
- name: redis
|
||||
installed_as: redis
|
||||
- name: smtp
|
||||
defaultConfig:
|
||||
namespace: decidim
|
||||
externalDnsDomain: "{{ .cloud.domain }}"
|
||||
@@ -26,12 +25,12 @@ defaultConfig:
|
||||
tlsSecretName: wildcard-wild-cloud-tls
|
||||
smtp:
|
||||
enabled: true
|
||||
host: "{{ .apps.smtp.host }}"
|
||||
port: "{{ .apps.smtp.port }}"
|
||||
user: "{{ .apps.smtp.user }}"
|
||||
from: "{{ .apps.smtp.from }}"
|
||||
tls: "{{ .apps.smtp.tls }}"
|
||||
startTls: "{{ .apps.smtp.startTls }}"
|
||||
host: "{{ .cloud.smtp.host }}"
|
||||
port: "{{ .cloud.smtp.port }}"
|
||||
user: "{{ .cloud.smtp.user }}"
|
||||
from: "{{ .cloud.smtp.from }}"
|
||||
tls: "{{ .cloud.smtp.tls }}"
|
||||
startTls: "{{ .cloud.smtp.startTls }}"
|
||||
defaultSecrets:
|
||||
- key: systemAdminPassword
|
||||
- key: secretKeyBase
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
# Discourse
|
||||
|
||||
Discourse is a modern, open-source discussion platform designed for online communities and forums.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **PostgreSQL** - Database for storing application data
|
||||
- **Redis** - Used for caching and background jobs
|
||||
|
||||
## Configuration
|
||||
|
||||
Key settings configured through your instance's `config.yaml`:
|
||||
|
||||
- **domain** - Where Discourse will be accessible (default: `discourse.{your-cloud-domain}`)
|
||||
- **adminEmail** - Admin account email (defaults to your operator email)
|
||||
- **adminUsername** - Admin account username (default: `admin`)
|
||||
- **siteName** - Your community name (default: `Community`)
|
||||
- **SMTP** - Email delivery settings inherited from your Wild Cloud instance
|
||||
|
||||
## Access
|
||||
|
||||
After deployment, Discourse will be available at:
|
||||
- `https://discourse.{your-cloud-domain}`
|
||||
|
||||
## First-Time Setup
|
||||
|
||||
1. Add and deploy the app:
|
||||
```bash
|
||||
wild app add discourse
|
||||
wild app deploy discourse
|
||||
```
|
||||
|
||||
2. Log in with the admin credentials configured during setup
|
||||
|
||||
3. Complete the setup wizard to configure your community
|
||||
@@ -6,7 +6,6 @@ icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/discourse.svg
|
||||
requires:
|
||||
- name: postgres
|
||||
- name: redis
|
||||
- name: smtp
|
||||
defaultConfig:
|
||||
namespace: discourse
|
||||
externalDnsDomain: "{{ .cloud.domain }}"
|
||||
@@ -25,12 +24,12 @@ defaultConfig:
|
||||
tlsSecretName: wildcard-wild-cloud-tls
|
||||
smtp:
|
||||
enabled: false
|
||||
host: "{{ .apps.smtp.host }}"
|
||||
port: "{{ .apps.smtp.port }}"
|
||||
user: "{{ .apps.smtp.user }}"
|
||||
from: "{{ .apps.smtp.from }}"
|
||||
tls: "{{ .apps.smtp.tls }}"
|
||||
startTls: "{{ .apps.smtp.startTls }}"
|
||||
host: "{{ .cloud.smtp.host }}"
|
||||
port: "{{ .cloud.smtp.port }}"
|
||||
user: "{{ .cloud.smtp.user }}"
|
||||
from: "{{ .cloud.smtp.from }}"
|
||||
tls: "{{ .cloud.smtp.tls }}"
|
||||
startTls: "{{ .cloud.smtp.startTls }}"
|
||||
defaultSecrets:
|
||||
- key: adminPassword
|
||||
- key: secretKeyBase
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: docker-registry
|
||||
labels:
|
||||
app: docker-registry
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: docker-registry
|
||||
strategy:
|
||||
rollingUpdate:
|
||||
maxSurge: 0
|
||||
maxUnavailable: 1
|
||||
type: RollingUpdate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: docker-registry
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
fsGroup: 1000
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- image: registry:3.0.0
|
||||
name: docker-registry
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
ports:
|
||||
- containerPort: 5000
|
||||
protocol: TCP
|
||||
volumeMounts:
|
||||
- mountPath: /var/lib/registry
|
||||
name: docker-registry-storage
|
||||
readOnly: false
|
||||
volumes:
|
||||
- name: docker-registry-storage
|
||||
persistentVolumeClaim:
|
||||
claimName: docker-registry-pvc
|
||||
@@ -1,20 +0,0 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: docker-registry
|
||||
spec:
|
||||
rules:
|
||||
- host: {{ .host }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: docker-registry
|
||||
port:
|
||||
number: 5000
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ .host }}
|
||||
secretName: wildcard-internal-wild-cloud-tls
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
if [ -z "${WILD_INSTANCE}" ]; then
|
||||
echo "ERROR: WILD_INSTANCE is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${WILD_API_DATA_DIR}" ]; then
|
||||
echo "ERROR: WILD_API_DATA_DIR is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${KUBECONFIG}" ]; then
|
||||
echo "ERROR: KUBECONFIG is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INSTANCE_DIR="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}"
|
||||
DOCKER_REGISTRY_DIR="${INSTANCE_DIR}/apps/docker-registry"
|
||||
|
||||
echo "=== Setting up Docker Registry ==="
|
||||
echo ""
|
||||
|
||||
echo "Using pre-compiled Docker Registry templates..."
|
||||
if [ ! -f "${DOCKER_REGISTRY_DIR}/kustomization.yaml" ]; then
|
||||
echo "ERROR: Compiled templates not found at ${DOCKER_REGISTRY_DIR}"
|
||||
echo "Templates should be compiled before deployment."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Deploying Docker Registry..."
|
||||
kubectl apply -k "${DOCKER_REGISTRY_DIR}/"
|
||||
|
||||
echo "Waiting for Docker Registry to be ready..."
|
||||
kubectl wait --for=condition=available --timeout=300s deployment/docker-registry -n docker-registry
|
||||
|
||||
echo ""
|
||||
echo "Docker Registry installed successfully"
|
||||
echo ""
|
||||
echo "Deployment status:"
|
||||
kubectl get pods -n docker-registry
|
||||
kubectl get services -n docker-registry
|
||||
echo ""
|
||||
echo "To use the registry:"
|
||||
echo " docker tag myimage registry.local/myimage"
|
||||
echo " docker push registry.local/myimage"
|
||||
@@ -1,14 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: docker-registry
|
||||
labels:
|
||||
- includeSelectors: true
|
||||
pairs:
|
||||
app: docker-registry
|
||||
managedBy: wild-cloud
|
||||
resources:
|
||||
- deployment.yaml
|
||||
- ingress.yaml
|
||||
- service.yaml
|
||||
- namespace.yaml
|
||||
- pvc.yaml
|
||||
@@ -1,12 +0,0 @@
|
||||
name: docker-registry
|
||||
is: docker-registry
|
||||
description: Private Docker image registry for cluster
|
||||
version: "3.0.0"
|
||||
namespace: docker-registry
|
||||
category: infrastructure
|
||||
requires:
|
||||
- name: traefik
|
||||
- name: cert-manager
|
||||
defaultConfig:
|
||||
host: "registry.{{ .cloud.internalDomain }}"
|
||||
storage: "100Gi"
|
||||
@@ -1,4 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: docker-registry
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: docker-registry
|
||||
labels:
|
||||
app: docker-registry
|
||||
spec:
|
||||
ports:
|
||||
- port: 5000
|
||||
targetPort: 5000
|
||||
selector:
|
||||
app: docker-registry
|
||||
@@ -1,9 +0,0 @@
|
||||
# Example Admin App
|
||||
|
||||
An example application deployed with internal-only access. This app is useful for testing Wild Cloud's internal ingress and TLS configuration.
|
||||
|
||||
The app uses the internal wildcard TLS certificate and is only accessible within your local network.
|
||||
|
||||
## Access
|
||||
|
||||
After deployment, the app will be available at an internal domain on your Wild Cloud instance.
|
||||
@@ -1,5 +1,6 @@
|
||||
name: example-admin
|
||||
is: example
|
||||
install: true
|
||||
description: An example application that is deployed with internal-only access.
|
||||
version: 1.0.0
|
||||
defaultConfig:
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# Example App
|
||||
|
||||
An example application deployed with public access. This app is useful for testing Wild Cloud's public ingress, TLS, and external DNS configuration.
|
||||
|
||||
The app uses the public wildcard TLS certificate and is accessible from the internet.
|
||||
|
||||
## Access
|
||||
|
||||
After deployment, the app will be available at:
|
||||
- `https://example-app.{your-cloud-domain}`
|
||||
@@ -1,5 +1,6 @@
|
||||
name: example-app
|
||||
is: example
|
||||
install: true
|
||||
description: An example application that is deployed with public access.
|
||||
version: 1.0.0
|
||||
defaultConfig:
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
# External DNS
|
||||
|
||||
See: https://github.com/kubernetes-sigs/external-dns
|
||||
|
||||
ExternalDNS allows you to keep selected zones (via --domain-filter) synchronized with Ingresses and Services of type=LoadBalancer and nodes in various DNS providers.
|
||||
|
||||
Currently, we are only configured to use CloudFlare.
|
||||
|
||||
Docs: https://github.com/kubernetes-sigs/external-dns/blob/master/docs/tutorials/cloudflare.md
|
||||
|
||||
Any Ingress that has metatdata.annotions with
|
||||
external-dns.alpha.kubernetes.io/hostname: `<something>.${DOMAIN}`
|
||||
|
||||
will have Cloudflare records created by External DNS.
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
namespace: externaldns
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
strategy:
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.k8s.io/external-dns/external-dns:v0.13.4
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
- --txt-owner-id={{ .ownerId }}
|
||||
- --provider=cloudflare
|
||||
- --domain-filter=payne.io
|
||||
#- --exclude-domains=internal.${DOMAIN}
|
||||
- --cloudflare-dns-records-per-page=5000
|
||||
- --publish-internal-services
|
||||
- --no-cloudflare-proxied
|
||||
- --log-level=debug
|
||||
env:
|
||||
- name: CF_API_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: cloudflare-api-token
|
||||
key: api-token
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
namespace: externaldns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["services", "endpoints", "pods"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
- apiGroups: ["extensions", "networking.k8s.io"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: external-dns
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: external-dns
|
||||
namespace: externaldns
|
||||
@@ -1,66 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
if [ -z "${WILD_INSTANCE}" ]; then
|
||||
echo "ERROR: WILD_INSTANCE is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${WILD_API_DATA_DIR}" ]; then
|
||||
echo "ERROR: WILD_API_DATA_DIR is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${KUBECONFIG}" ]; then
|
||||
echo "ERROR: KUBECONFIG is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INSTANCE_DIR="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}"
|
||||
EXTERNALDNS_DIR="${INSTANCE_DIR}/apps/externaldns"
|
||||
|
||||
echo "=== Setting up ExternalDNS ==="
|
||||
echo ""
|
||||
|
||||
echo "Verifying cert-manager is ready (required for ExternalDNS)..."
|
||||
kubectl wait --for=condition=Available deployment/cert-manager -n cert-manager --timeout=60s 2>/dev/null && \
|
||||
kubectl wait --for=condition=Available deployment/cert-manager-webhook -n cert-manager --timeout=60s 2>/dev/null || {
|
||||
echo "cert-manager not ready, but continuing with ExternalDNS installation"
|
||||
echo "Note: ExternalDNS may not work properly without cert-manager"
|
||||
}
|
||||
|
||||
echo "Using pre-compiled ExternalDNS templates..."
|
||||
if [ ! -f "${EXTERNALDNS_DIR}/kustomization.yaml" ]; then
|
||||
echo "ERROR: Compiled templates not found at ${EXTERNALDNS_DIR}"
|
||||
echo "Templates should be compiled before deployment."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Deploying ExternalDNS..."
|
||||
kubectl apply -k ${EXTERNALDNS_DIR}/
|
||||
|
||||
echo "Creating Cloudflare API token secret..."
|
||||
SECRETS_FILE="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}/secrets.yaml"
|
||||
CLOUDFLARE_API_TOKEN=$(yq '.apps.externaldns.cert-manager\.cloudflareToken' "$SECRETS_FILE" 2>/dev/null | tr -d '"')
|
||||
|
||||
if [ -z "$CLOUDFLARE_API_TOKEN" ] || [ "$CLOUDFLARE_API_TOKEN" = "null" ]; then
|
||||
echo "ERROR: Cloudflare API token not found."
|
||||
echo "Please ensure cert-manager has been added with a cloudflareToken secret."
|
||||
exit 1
|
||||
fi
|
||||
kubectl create secret generic cloudflare-api-token \
|
||||
--namespace externaldns \
|
||||
--from-literal=api-token="${CLOUDFLARE_API_TOKEN}" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
echo "Waiting for Cloudflare ExternalDNS to be ready..."
|
||||
kubectl rollout status deployment/external-dns -n externaldns --timeout=60s
|
||||
|
||||
echo ""
|
||||
echo "ExternalDNS installed successfully"
|
||||
echo ""
|
||||
echo "To verify the installation:"
|
||||
echo " kubectl get pods -n externaldns"
|
||||
echo " kubectl logs -n externaldns -l app=external-dns -f"
|
||||
echo ""
|
||||
@@ -1,7 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- externaldns-rbac.yaml
|
||||
- externaldns-cloudflare.yaml
|
||||
@@ -1,15 +0,0 @@
|
||||
name: externaldns
|
||||
is: externaldns
|
||||
description: Automatically configures DNS records for services
|
||||
version: v0.13.4
|
||||
namespace: externaldns
|
||||
deploymentName: external-dns
|
||||
category: infrastructure
|
||||
requires:
|
||||
- name: cert-manager
|
||||
defaultConfig:
|
||||
ownerId: "wild-cloud-{{ .cluster.name }}"
|
||||
defaultSecrets:
|
||||
- key: cloudflareToken
|
||||
requiredSecrets:
|
||||
- cert-manager.cloudflareToken
|
||||
@@ -1,4 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: externaldns
|
||||
@@ -1,33 +0,0 @@
|
||||
# Ghost
|
||||
|
||||
Ghost is a powerful app for new-media creators to publish, share, and grow a business around their content. It provides a clean writing experience with built-in membership and subscription features.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **MySQL** - Database for storing content and configuration
|
||||
|
||||
## Configuration
|
||||
|
||||
Key settings configured through your instance's `config.yaml`:
|
||||
|
||||
- **domain** - Where Ghost will be accessible (default: `ghost.{your-cloud-domain}`)
|
||||
- **blogTitle** - Your blog's title (default: `My Blog`)
|
||||
- **adminEmail** - Admin account email (defaults to your operator email)
|
||||
- **storage** - Persistent volume size for content (default: `10Gi`)
|
||||
- **SMTP** - Email delivery settings inherited from your Wild Cloud instance
|
||||
|
||||
## Access
|
||||
|
||||
After deployment, Ghost will be available at:
|
||||
- `https://ghost.{your-cloud-domain}` - Public blog
|
||||
- `https://ghost.{your-cloud-domain}/ghost` - Admin panel
|
||||
|
||||
## First-Time Setup
|
||||
|
||||
1. Add and deploy the app:
|
||||
```bash
|
||||
wild app add ghost
|
||||
wild app deploy ghost
|
||||
```
|
||||
|
||||
2. Navigate to the admin panel and create your first post
|
||||
@@ -6,7 +6,6 @@ version: 5.118.1
|
||||
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/ghost.png
|
||||
requires:
|
||||
- name: mysql
|
||||
- name: smtp
|
||||
defaultConfig:
|
||||
namespace: ghost
|
||||
externalDnsDomain: '{{ .cloud.domain }}'
|
||||
@@ -24,10 +23,10 @@ defaultConfig:
|
||||
blogTitle: My Blog
|
||||
timezone: UTC
|
||||
smtp:
|
||||
host: '{{ .apps.smtp.host }}'
|
||||
port: '{{ .apps.smtp.port }}'
|
||||
from: '{{ .apps.smtp.from }}'
|
||||
user: '{{ .apps.smtp.user }}'
|
||||
host: '{{ .cloud.smtp.host }}'
|
||||
port: '{{ .cloud.smtp.port }}'
|
||||
from: '{{ .cloud.smtp.from }}'
|
||||
user: '{{ .cloud.smtp.user }}'
|
||||
defaultSecrets:
|
||||
- key: adminPassword
|
||||
- key: dbPassword
|
||||
|
||||
@@ -5,7 +5,6 @@ version: 1.24.3
|
||||
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gitea.svg
|
||||
requires:
|
||||
- name: postgres
|
||||
- name: smtp
|
||||
defaultConfig:
|
||||
namespace: gitea
|
||||
externalDnsDomain: '{{ .cloud.domain }}'
|
||||
@@ -25,10 +24,10 @@ defaultConfig:
|
||||
timezone: UTC
|
||||
runMode: prod
|
||||
smtp:
|
||||
host: '{{ .apps.smtp.host }}'
|
||||
port: '{{ .apps.smtp.port }}'
|
||||
user: '{{ .apps.smtp.user }}'
|
||||
from: '{{ .apps.smtp.from }}'
|
||||
host: '{{ .cloud.smtp.host }}'
|
||||
port: '{{ .cloud.smtp.port }}'
|
||||
user: '{{ .cloud.smtp.user }}'
|
||||
from: '{{ .cloud.smtp.from }}'
|
||||
defaultSecrets:
|
||||
- key: adminPassword
|
||||
- key: dbPassword
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: headlamp
|
||||
namespace: headlamp
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: headlamp
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: headlamp
|
||||
spec:
|
||||
serviceAccountName: headlamp-admin
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 100
|
||||
runAsGroup: 101
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: headlamp
|
||||
image: ghcr.io/headlamp-k8s/headlamp:v0.42.0
|
||||
args:
|
||||
- "-in-cluster"
|
||||
- "-plugins-dir=/headlamp/plugins"
|
||||
- "-kubeconfig=/home/headlamp/.kube/config"
|
||||
ports:
|
||||
- containerPort: 4466
|
||||
name: http
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: [ALL]
|
||||
readOnlyRootFilesystem: false
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 4466
|
||||
initialDelaySeconds: 10
|
||||
timeoutSeconds: 5
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 4466
|
||||
initialDelaySeconds: 15
|
||||
timeoutSeconds: 5
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
memory: 256Mi
|
||||
volumeMounts:
|
||||
- name: kubeconfig
|
||||
mountPath: /home/headlamp/.kube
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: kubeconfig
|
||||
configMap:
|
||||
name: headlamp-kubeconfig
|
||||
items:
|
||||
- key: kubeconfig
|
||||
path: config
|
||||
nodeSelector:
|
||||
kubernetes.io/os: linux
|
||||
@@ -1,64 +0,0 @@
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: internal-only
|
||||
namespace: headlamp
|
||||
spec:
|
||||
ipWhiteList:
|
||||
sourceRange:
|
||||
- 127.0.0.1/32
|
||||
- 10.0.0.0/8
|
||||
- 172.16.0.0/12
|
||||
- 192.168.0.0/16
|
||||
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: headlamp-redirect-scheme
|
||||
namespace: headlamp
|
||||
spec:
|
||||
redirectScheme:
|
||||
scheme: https
|
||||
permanent: true
|
||||
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: headlamp-https
|
||||
namespace: headlamp
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`headlamp.{{ .internalDomain }}`)
|
||||
kind: Rule
|
||||
middlewares:
|
||||
- name: internal-only
|
||||
namespace: headlamp
|
||||
services:
|
||||
- name: headlamp
|
||||
port: 80
|
||||
tls:
|
||||
secretName: wildcard-internal-wild-cloud-tls
|
||||
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: headlamp-http
|
||||
namespace: headlamp
|
||||
spec:
|
||||
entryPoints:
|
||||
- web
|
||||
routes:
|
||||
- match: Host(`headlamp.{{ .internalDomain }}`)
|
||||
kind: Rule
|
||||
middlewares:
|
||||
- name: headlamp-redirect-scheme
|
||||
namespace: headlamp
|
||||
services:
|
||||
- name: headlamp
|
||||
port: 80
|
||||
@@ -1,63 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
if [ -z "${WILD_INSTANCE}" ]; then
|
||||
echo "ERROR: WILD_INSTANCE is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${WILD_API_DATA_DIR}" ]; then
|
||||
echo "ERROR: WILD_API_DATA_DIR is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${KUBECONFIG}" ]; then
|
||||
echo "ERROR: KUBECONFIG is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INSTANCE_DIR="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}"
|
||||
HEADLAMP_DIR="${INSTANCE_DIR}/apps/headlamp"
|
||||
|
||||
echo "=== Setting up Headlamp ==="
|
||||
echo ""
|
||||
|
||||
echo "Using pre-compiled Headlamp templates..."
|
||||
if [ ! -f "${HEADLAMP_DIR}/kustomization.yaml" ]; then
|
||||
echo "ERROR: Compiled templates not found at ${HEADLAMP_DIR}"
|
||||
echo "Templates should be compiled before deployment."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Waiting for cert-manager certificates to be ready..."
|
||||
kubectl wait --for=condition=Ready certificate wildcard-internal-wild-cloud -n cert-manager --timeout=300s || echo "Warning: Internal wildcard certificate not ready yet"
|
||||
|
||||
NAMESPACE="headlamp"
|
||||
|
||||
echo "Copying cert-manager secrets to headlamp namespace..."
|
||||
kubectl create namespace ${NAMESPACE} --dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
if kubectl get secret wildcard-internal-wild-cloud-tls -n cert-manager >/dev/null 2>&1; then
|
||||
kubectl get secret wildcard-internal-wild-cloud-tls -n cert-manager -o yaml | \
|
||||
sed "s/namespace: cert-manager/namespace: ${NAMESPACE}/" | \
|
||||
kubectl apply -f -
|
||||
else
|
||||
echo "Warning: wildcard-internal-wild-cloud-tls secret not yet available"
|
||||
fi
|
||||
|
||||
echo "Deploying Headlamp..."
|
||||
kubectl apply -k "${HEADLAMP_DIR}/"
|
||||
|
||||
echo "Waiting for Headlamp to be ready..."
|
||||
kubectl rollout status deployment/headlamp -n ${NAMESPACE} --timeout=120s
|
||||
|
||||
echo ""
|
||||
echo "Headlamp installed successfully"
|
||||
echo ""
|
||||
if [ -n "${INTERNAL_DOMAIN}" ]; then
|
||||
echo "Access Headlamp at: https://headlamp.${INTERNAL_DOMAIN}"
|
||||
else
|
||||
echo "Access Headlamp via the configured internal domain"
|
||||
fi
|
||||
echo ""
|
||||
@@ -1,24 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: headlamp-kubeconfig
|
||||
namespace: headlamp
|
||||
data:
|
||||
kubeconfig: |
|
||||
apiVersion: v1
|
||||
kind: Config
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
|
||||
server: https://kubernetes.default.svc
|
||||
name: in-cluster
|
||||
contexts:
|
||||
- context:
|
||||
cluster: in-cluster
|
||||
user: headlamp-admin
|
||||
name: in-cluster
|
||||
current-context: in-cluster
|
||||
users:
|
||||
- name: headlamp-admin
|
||||
user:
|
||||
tokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||
@@ -1,16 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: headlamp
|
||||
labels:
|
||||
- includeSelectors: true
|
||||
pairs:
|
||||
app: headlamp
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- service-account.yaml
|
||||
- kubeconfig-cm.yaml
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
- ingress.yaml
|
||||
@@ -1,11 +0,0 @@
|
||||
name: headlamp
|
||||
is: headlamp
|
||||
description: Modern Kubernetes web UI (SIG UI) with in-cluster authentication
|
||||
version: v0.42.0
|
||||
namespace: headlamp
|
||||
category: infrastructure
|
||||
requires:
|
||||
- name: traefik
|
||||
- name: cert-manager
|
||||
defaultConfig:
|
||||
internalDomain: "{{ .cloud.internalDomain }}"
|
||||
@@ -1,4 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: headlamp
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: headlamp-admin
|
||||
namespace: headlamp
|
||||
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: headlamp-admin
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp-admin
|
||||
namespace: headlamp
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: cluster-admin
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
@@ -1,11 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: headlamp
|
||||
namespace: headlamp
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 4466
|
||||
selector:
|
||||
app: headlamp
|
||||
@@ -1,41 +1,7 @@
|
||||
# Immich
|
||||
# Immich App
|
||||
|
||||
Immich is a self-hosted photo and video backup solution that allows you to store, manage, and share your media files securely. It provides a mobile-first experience similar to Google Photos.
|
||||
## To Do
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **PostgreSQL** - Database for storing metadata and search indexes
|
||||
- **Redis** - Used for caching and background job queuing
|
||||
|
||||
## Components
|
||||
|
||||
Immich runs two services:
|
||||
|
||||
- **Server** - Main API and web server
|
||||
- **Machine Learning** - Handles facial recognition and smart search
|
||||
|
||||
## Configuration
|
||||
|
||||
Key settings configured through your instance's `config.yaml`:
|
||||
|
||||
- **domain** - Where Immich will be accessible (default: `immich.{your-cloud-domain}`)
|
||||
- **storage** - Persistent volume for photos and videos (default: `250Gi`)
|
||||
- **cacheStorage** - Persistent volume for ML cache (default: `10Gi`)
|
||||
- **timezone** - Server timezone (default: `UTC`)
|
||||
|
||||
## Access
|
||||
|
||||
After deployment, Immich will be available at:
|
||||
- `https://immich.{your-cloud-domain}`
|
||||
|
||||
## First-Time Setup
|
||||
|
||||
1. Add and deploy the app:
|
||||
```bash
|
||||
wild app add immich
|
||||
wild app deploy immich
|
||||
```
|
||||
|
||||
2. Create your account through the web interface
|
||||
|
||||
3. Download the Immich mobile app and connect it to your server for automatic photo backup
|
||||
- We need a full uninstall script.
|
||||
- We need full backup and restore scripts.
|
||||
- When recreating the app (uninstall/reinstall), db-init needs to re-run (currently the previous one blocks).
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
name: immich
|
||||
is: immich
|
||||
install: true
|
||||
description: Immich is a self-hosted photo and video backup solution that allows you
|
||||
to store, manage, and share your media files securely.
|
||||
version: release
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# Keila
|
||||
|
||||
Keila is an open-source email marketing platform that allows you to send newsletters and manage mailing lists with privacy and control.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **PostgreSQL** - Database for storing contacts and campaigns
|
||||
|
||||
## Configuration
|
||||
|
||||
Key settings configured through your instance's `config.yaml`:
|
||||
|
||||
- **domain** - Where Keila will be accessible (default: `keila.{your-cloud-domain}`)
|
||||
- **adminUser** - Admin account email (default: `admin@{your-cloud-domain}`)
|
||||
- **disableRegistration** - Whether to allow new signups (default: `true`)
|
||||
- **SMTP** - Email delivery settings inherited from your Wild Cloud instance
|
||||
|
||||
## Access
|
||||
|
||||
After deployment, Keila will be available at:
|
||||
- `https://keila.{your-cloud-domain}`
|
||||
|
||||
## First-Time Setup
|
||||
|
||||
1. Add and deploy the app:
|
||||
```bash
|
||||
wild app add keila
|
||||
wild app deploy keila
|
||||
```
|
||||
|
||||
2. Log in with the admin credentials configured during setup
|
||||
|
||||
3. Configure your SMTP sender and create your first campaign
|
||||
@@ -5,7 +5,6 @@ version: 0.17.1
|
||||
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/keila.svg
|
||||
requires:
|
||||
- name: postgres
|
||||
- name: smtp
|
||||
defaultConfig:
|
||||
namespace: keila
|
||||
externalDnsDomain: "{{ .cloud.domain }}"
|
||||
@@ -21,12 +20,12 @@ defaultConfig:
|
||||
adminUser: admin@{{ .cloud.domain }}
|
||||
tlsSecretName: wildcard-wild-cloud-tls
|
||||
smtp:
|
||||
host: "{{ .apps.smtp.host }}"
|
||||
port: "{{ .apps.smtp.port }}"
|
||||
from: "{{ .apps.smtp.from }}"
|
||||
user: "{{ .apps.smtp.user }}"
|
||||
tls: "{{ .apps.smtp.tls }}"
|
||||
startTls: "{{ .apps.smtp.startTls }}"
|
||||
host: "{{ .cloud.smtp.host }}"
|
||||
port: "{{ .cloud.smtp.port }}"
|
||||
from: "{{ .cloud.smtp.from }}"
|
||||
user: "{{ .cloud.smtp.user }}"
|
||||
tls: "{{ .cloud.smtp.tls }}"
|
||||
startTls: "{{ .cloud.smtp.startTls }}"
|
||||
defaultSecrets:
|
||||
- key: secretKeyBase
|
||||
default: "{{ random.AlphaNum 64 }}"
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
# Lemmy
|
||||
|
||||
Lemmy is a selfhosted social link aggregation and discussion platform. It is an open-source alternative to Reddit, designed for the fediverse.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **PostgreSQL** - Database for storing communities, posts, and comments
|
||||
|
||||
## Components
|
||||
|
||||
Lemmy runs three separate services:
|
||||
|
||||
- **Backend** - Rust API server handling federation and data
|
||||
- **UI** - Web frontend for browsing and interacting
|
||||
- **pict-rs** - Image hosting and processing service
|
||||
|
||||
## Configuration
|
||||
|
||||
Key settings configured through your instance's `config.yaml`:
|
||||
|
||||
- **domain** - Where Lemmy will be accessible (default: `lemmy.{your-cloud-domain}`)
|
||||
- **storage** - Persistent volume for application data (default: `10Gi`)
|
||||
- **pictrsStorage** - Persistent volume for uploaded images (default: `50Gi`)
|
||||
- **SMTP** - Email delivery settings inherited from your Wild Cloud instance
|
||||
|
||||
## Access
|
||||
|
||||
After deployment, Lemmy will be available at:
|
||||
- `https://lemmy.{your-cloud-domain}`
|
||||
|
||||
## First-Time Setup
|
||||
|
||||
1. Add and deploy the app:
|
||||
```bash
|
||||
wild app add lemmy
|
||||
wild app deploy lemmy
|
||||
```
|
||||
|
||||
2. Create your admin account through the web interface
|
||||
|
||||
3. Set up your first community and customize your instance settings
|
||||
@@ -5,7 +5,6 @@ version: 0.19.15
|
||||
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/lemmy.svg
|
||||
requires:
|
||||
- name: postgres
|
||||
- name: smtp
|
||||
defaultConfig:
|
||||
namespace: lemmy
|
||||
backendImage: dessalines/lemmy:0.19.15
|
||||
@@ -28,11 +27,11 @@ defaultConfig:
|
||||
dbHost: postgres.postgres.svc.cluster.local
|
||||
dbPort: 5432
|
||||
smtp:
|
||||
host: "{{ .apps.smtp.host }}"
|
||||
port: "{{ .apps.smtp.port }}"
|
||||
user: "{{ .apps.smtp.user }}"
|
||||
host: "{{ .cloud.smtp.host }}"
|
||||
port: "{{ .cloud.smtp.port }}"
|
||||
user: "{{ .cloud.smtp.user }}"
|
||||
from: "noreply@{{ .cloud.baseDomain }}"
|
||||
tls: "{{ .apps.smtp.tls }}"
|
||||
tls: "{{ .cloud.smtp.tls }}"
|
||||
defaultSecrets:
|
||||
- key: dbPassword
|
||||
- key: adminPassword
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
# Listmonk
|
||||
|
||||
Listmonk is a standalone, self-hosted newsletter and mailing list manager. It is fast, feature-rich, and packed into a single binary.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **PostgreSQL** - Database for storing subscribers and campaigns
|
||||
|
||||
## Configuration
|
||||
|
||||
Key settings configured through your instance's `config.yaml`:
|
||||
|
||||
- **domain** - Where Listmonk will be accessible (default: `listmonk.{your-cloud-domain}`)
|
||||
- **storage** - Persistent volume size (default: `1Gi`)
|
||||
|
||||
## Access
|
||||
|
||||
After deployment, Listmonk will be available at:
|
||||
- `https://listmonk.{your-cloud-domain}`
|
||||
|
||||
## First-Time Setup
|
||||
|
||||
1. Add and deploy the app:
|
||||
```bash
|
||||
wild app add listmonk
|
||||
wild app deploy listmonk
|
||||
```
|
||||
|
||||
2. Log in to the admin interface and configure your SMTP settings for sending emails
|
||||
|
||||
3. Create your first mailing list and start adding subscribers
|
||||
@@ -1,20 +0,0 @@
|
||||
# Longhorn Storage
|
||||
|
||||
See: [Longhorn Docs v 1.8.1](https://longhorn.io/docs/1.8.1/deploy/install/install-with-kubectl/)
|
||||
|
||||
## Installation Notes
|
||||
|
||||
- Manifest copied from https://raw.githubusercontent.com/longhorn/longhorn/v1.8.1/deploy/longhorn.yaml
|
||||
- Using kustomize to apply custom configuration (see `kustomization.yaml`)
|
||||
|
||||
## Important Settings
|
||||
|
||||
- **Number of Replicas**: Set to 1 (default is 3) to accommodate smaller clusters
|
||||
- This avoids "degraded" volumes when fewer than 3 nodes are available
|
||||
- For production with 3+ nodes, consider changing back to 3 for better availability
|
||||
|
||||
## Common Operations
|
||||
|
||||
- View volumes: `kubectl get volumes.longhorn.io -n longhorn-system`
|
||||
- Check volume status: `kubectl describe volumes.longhorn.io <volume-name> -n longhorn-system`
|
||||
- Access Longhorn UI: Set up port-forwarding with `kubectl -n longhorn-system port-forward service/longhorn-frontend 8080:80`
|
||||
@@ -1,21 +0,0 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: longhorn-ingress
|
||||
namespace: longhorn-system
|
||||
spec:
|
||||
rules:
|
||||
- host: "longhorn.{{ .internalDomain }}"
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: longhorn-frontend
|
||||
port:
|
||||
number: 80
|
||||
tls:
|
||||
- secretName: wildcard-internal-wild-cloud-tls
|
||||
hosts:
|
||||
- "longhorn.{{ .internalDomain }}"
|
||||
@@ -1,47 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
if [ -z "${WILD_INSTANCE}" ]; then
|
||||
echo "ERROR: WILD_INSTANCE is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${WILD_API_DATA_DIR}" ]; then
|
||||
echo "ERROR: WILD_API_DATA_DIR is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${KUBECONFIG}" ]; then
|
||||
echo "ERROR: KUBECONFIG is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INSTANCE_DIR="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}"
|
||||
LONGHORN_DIR="${INSTANCE_DIR}/apps/longhorn"
|
||||
|
||||
echo "=== Setting up Longhorn ==="
|
||||
echo ""
|
||||
|
||||
echo "Using pre-compiled Longhorn templates..."
|
||||
if [ ! -f "${LONGHORN_DIR}/kustomization.yaml" ]; then
|
||||
echo "ERROR: Compiled templates not found at ${LONGHORN_DIR}"
|
||||
echo "Templates should be compiled before deployment."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Deploying Longhorn..."
|
||||
kubectl apply -k ${LONGHORN_DIR}/
|
||||
|
||||
echo "Waiting for Longhorn to be ready..."
|
||||
kubectl wait --for=condition=available --timeout=300s deployment/longhorn-driver-deployer -n longhorn-system || true
|
||||
|
||||
echo ""
|
||||
echo "Longhorn installed successfully"
|
||||
echo ""
|
||||
echo "To verify the installation:"
|
||||
echo " kubectl get pods -n longhorn-system"
|
||||
echo " kubectl get storageclass"
|
||||
echo ""
|
||||
echo "To access the Longhorn UI:"
|
||||
echo " kubectl port-forward -n longhorn-system svc/longhorn-frontend 8080:80"
|
||||
@@ -1,7 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- longhorn.yaml
|
||||
- ingress.yaml
|
||||
- volumesnapshotclass-longhorn.yaml
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
||||
name: longhorn
|
||||
is: longhorn
|
||||
description: Cloud-native distributed block storage for Kubernetes
|
||||
version: v1.8.1
|
||||
namespace: longhorn-system
|
||||
deploymentName: longhorn-ui
|
||||
category: infrastructure
|
||||
requires:
|
||||
- name: traefik
|
||||
- name: nfs
|
||||
defaultConfig:
|
||||
internalDomain: "{{ .cloud.internalDomain }}"
|
||||
backupTarget: "nfs://{{ .apps.nfs.host }}:/data/{{ .cluster.name }}/backups"
|
||||
@@ -1,8 +0,0 @@
|
||||
apiVersion: snapshot.storage.k8s.io/v1
|
||||
kind: VolumeSnapshotClass
|
||||
metadata:
|
||||
name: longhorn-snapshot-class
|
||||
driver: driver.longhorn.io
|
||||
deletionPolicy: Delete
|
||||
parameters:
|
||||
type: snap
|
||||
@@ -1,34 +0,0 @@
|
||||
# Loomio
|
||||
|
||||
Loomio is a collaborative decision-making tool that makes it easy for groups to make decisions together. It supports proposals, polls, and structured discussions.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **PostgreSQL** - Database for storing groups, discussions, and decisions
|
||||
- **Redis** - Used for caching and background jobs
|
||||
|
||||
## Configuration
|
||||
|
||||
Key settings configured through your instance's `config.yaml`:
|
||||
|
||||
- **domain** - Where Loomio will be accessible (default: `loomio.{your-cloud-domain}`)
|
||||
- **appName** - Display name for your instance (default: `Loomio`)
|
||||
- **adminEmail** - Admin contact email (defaults to your operator email)
|
||||
- **SMTP** - Email delivery settings inherited from your Wild Cloud instance
|
||||
|
||||
## Access
|
||||
|
||||
After deployment, Loomio will be available at:
|
||||
- `https://loomio.{your-cloud-domain}`
|
||||
|
||||
## First-Time Setup
|
||||
|
||||
1. Add and deploy the app:
|
||||
```bash
|
||||
wild app add loomio
|
||||
wild app deploy loomio
|
||||
```
|
||||
|
||||
2. Create your account and set up your first group
|
||||
|
||||
3. Invite members and start a discussion or poll
|
||||
@@ -7,7 +7,6 @@ requires:
|
||||
- name: postgres
|
||||
installed_as: postgres
|
||||
- name: redis
|
||||
- name: smtp
|
||||
defaultConfig:
|
||||
namespace: loomio
|
||||
externalDnsDomain: "{{ .cloud.domain }}"
|
||||
@@ -38,11 +37,11 @@ defaultConfig:
|
||||
smtp:
|
||||
auth: plain
|
||||
domain: "{{ .cloud.domain }}"
|
||||
host: "{{ .apps.smtp.host }}"
|
||||
port: "{{ .apps.smtp.port }}"
|
||||
user: "{{ .apps.smtp.user }}"
|
||||
tls: "{{ .apps.smtp.tls }}"
|
||||
from: "{{ .apps.smtp.from }}"
|
||||
host: "{{ .cloud.smtp.host }}"
|
||||
port: "{{ .cloud.smtp.port }}"
|
||||
user: "{{ .cloud.smtp.user }}"
|
||||
tls: "{{ .cloud.smtp.tls }}"
|
||||
from: "{{ .cloud.smtp.from }}"
|
||||
defaultSecrets:
|
||||
- key: dbPassword
|
||||
default: "{{ random.AlphaNum 32 }}"
|
||||
|
||||
30
mailu/configmap.yaml
Normal file
30
mailu/configmap.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: mailu-config
|
||||
namespace: {{ .namespace }}
|
||||
data:
|
||||
DOMAIN: "{{ .domain }}"
|
||||
HOSTNAMES: "{{ .hostname }}"
|
||||
POSTMASTER: "admin"
|
||||
TZ: "{{ .timezone }}"
|
||||
TLS_FLAVOR: "cert"
|
||||
MESSAGE_SIZE_LIMIT: "50000000"
|
||||
MESSAGE_RATELIMIT: "200/day"
|
||||
RELAYNETS: ""
|
||||
RELAYHOST: "{{ .relayHost }}"
|
||||
RELAYPORT: "{{ .relayPort }}"
|
||||
FETCHMAIL_ENABLED: "false"
|
||||
RECIPIENT_DELIMITER: "+"
|
||||
DMARC_RUA: "admin"
|
||||
DMARC_RUF: "admin"
|
||||
WELCOME: "false"
|
||||
WELCOME_SUBJECT: "Welcome to your new email account"
|
||||
WELCOME_BODY: "Welcome! You can now use your email account."
|
||||
ADMIN: "true"
|
||||
WEB_ADMIN: "/admin"
|
||||
WEB_WEBMAIL: "/webmail"
|
||||
WEBMAIL: "roundcube"
|
||||
SITENAME: "Mailu"
|
||||
WEBSITE: "https://{{ .hostname }}"
|
||||
LOG_LEVEL: "{{ .logLevel }}"
|
||||
103
mailu/deployment-admin.yaml
Normal file
103
mailu/deployment-admin.yaml
Normal file
@@ -0,0 +1,103 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: admin
|
||||
namespace: {{ .namespace }}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
component: admin
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
component: admin
|
||||
spec:
|
||||
dnsPolicy: "None"
|
||||
dnsConfig:
|
||||
nameservers:
|
||||
- {{ .unbound.ip }}
|
||||
searches:
|
||||
- {{ .namespace }}.svc.cluster.local
|
||||
- svc.cluster.local
|
||||
- cluster.local
|
||||
options:
|
||||
- name: ndots
|
||||
value: "5"
|
||||
initContainers:
|
||||
- name: fix-permissions
|
||||
image: busybox:latest
|
||||
command: ['sh', '-c', 'chown -R 999:999 /data /dkim']
|
||||
volumeMounts:
|
||||
- name: data
|
||||
subPath: admin
|
||||
mountPath: /data
|
||||
- name: data
|
||||
subPath: dkim
|
||||
mountPath: /dkim
|
||||
containers:
|
||||
- name: admin
|
||||
image: {{ .images.admin }}
|
||||
imagePullPolicy: IfNotPresent
|
||||
securityContext:
|
||||
capabilities:
|
||||
add:
|
||||
- SYS_CHROOT
|
||||
- CHOWN
|
||||
- SETGID
|
||||
- SETUID
|
||||
env:
|
||||
- name: SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mailu-secrets
|
||||
key: secretKey
|
||||
- name: REDIS_ADDRESS
|
||||
value: "{{ .redis.host }}"
|
||||
- name: I_KNOW_MY_SETUP_DOESNT_FIT_REQUIREMENTS_AND_WONT_FILE_ISSUES_WITHOUT_PATCHES
|
||||
value: "true"
|
||||
- name: INITIAL_ADMIN_ACCOUNT
|
||||
value: "{{ .initialAccount.username }}"
|
||||
- name: INITIAL_ADMIN_DOMAIN
|
||||
value: "{{ .initialAccount.domain }}"
|
||||
- name: INITIAL_ADMIN_PW
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mailu-secrets
|
||||
key: initialAccountPassword
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: mailu-config
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
volumeMounts:
|
||||
- name: data
|
||||
subPath: admin
|
||||
mountPath: /data
|
||||
- name: data
|
||||
subPath: dkim
|
||||
mountPath: /dkim
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "1000m"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: 8080
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: mailu-storage
|
||||
70
mailu/deployment-dovecot.yaml
Normal file
70
mailu/deployment-dovecot.yaml
Normal file
@@ -0,0 +1,70 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: dovecot
|
||||
namespace: {{ .namespace }}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
component: dovecot
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
component: dovecot
|
||||
spec:
|
||||
initContainers:
|
||||
- name: fix-permissions
|
||||
image: busybox:latest
|
||||
command: ['sh', '-c', 'chown -R 999:999 /data /mail']
|
||||
volumeMounts:
|
||||
- name: data
|
||||
subPath: mail
|
||||
mountPath: /mail
|
||||
- name: data
|
||||
subPath: dovecot
|
||||
mountPath: /data
|
||||
containers:
|
||||
- name: dovecot
|
||||
image: {{ .images.dovecot }}
|
||||
imagePullPolicy: IfNotPresent
|
||||
securityContext:
|
||||
capabilities:
|
||||
add:
|
||||
- SYS_CHROOT
|
||||
- CHOWN
|
||||
- SETGID
|
||||
- SETUID
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: mailu-config
|
||||
ports:
|
||||
- name: imap
|
||||
containerPort: 143
|
||||
- name: imaps
|
||||
containerPort: 993
|
||||
- name: pop3
|
||||
containerPort: 110
|
||||
- name: pop3s
|
||||
containerPort: 995
|
||||
- name: sieve
|
||||
containerPort: 4190
|
||||
- name: auth
|
||||
containerPort: 2102
|
||||
- name: lmtp
|
||||
containerPort: 2525
|
||||
volumeMounts:
|
||||
- name: data
|
||||
subPath: mail
|
||||
mountPath: /mail
|
||||
resources:
|
||||
requests:
|
||||
memory: "1Gi"
|
||||
cpu: "500m"
|
||||
limits:
|
||||
memory: "2Gi"
|
||||
cpu: "2000m"
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: mailu-storage
|
||||
70
mailu/deployment-front.yaml
Normal file
70
mailu/deployment-front.yaml
Normal file
@@ -0,0 +1,70 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: front
|
||||
namespace: {{ .namespace }}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
component: front
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
component: front
|
||||
spec:
|
||||
containers:
|
||||
- name: front
|
||||
image: {{ .images.front }}
|
||||
imagePullPolicy: IfNotPresent
|
||||
securityContext:
|
||||
capabilities:
|
||||
add:
|
||||
- SYS_CHROOT
|
||||
- CHOWN
|
||||
- SETGID
|
||||
- SETUID
|
||||
- NET_BIND_SERVICE
|
||||
env:
|
||||
- name: SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mailu-secrets
|
||||
key: secretKey
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: mailu-config
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
- name: https
|
||||
containerPort: 443
|
||||
- name: smtp
|
||||
containerPort: 25
|
||||
- name: smtps
|
||||
containerPort: 465
|
||||
- name: submission
|
||||
containerPort: 587
|
||||
- name: imap
|
||||
containerPort: 143
|
||||
- name: imaps
|
||||
containerPort: 993
|
||||
- name: pop3
|
||||
containerPort: 110
|
||||
- name: pop3s
|
||||
containerPort: 995
|
||||
volumeMounts:
|
||||
- name: certs
|
||||
mountPath: /certs
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
volumes:
|
||||
- name: certs
|
||||
secret:
|
||||
secretName: {{ .tlsSecretName }}
|
||||
optional: true
|
||||
60
mailu/deployment-postfix.yaml
Normal file
60
mailu/deployment-postfix.yaml
Normal file
@@ -0,0 +1,60 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: postfix
|
||||
namespace: {{ .namespace }}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
component: postfix
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
component: postfix
|
||||
spec:
|
||||
initContainers:
|
||||
- name: fix-permissions
|
||||
image: busybox:latest
|
||||
command: ['sh', '-c', 'chown -R 999:999 /queue']
|
||||
volumeMounts:
|
||||
- name: data
|
||||
subPath: mailqueue
|
||||
mountPath: /queue
|
||||
containers:
|
||||
- name: postfix
|
||||
image: {{ .images.postfix }}
|
||||
imagePullPolicy: IfNotPresent
|
||||
securityContext:
|
||||
capabilities:
|
||||
add:
|
||||
- SYS_CHROOT
|
||||
- CHOWN
|
||||
- SETGID
|
||||
- SETUID
|
||||
- NET_BIND_SERVICE
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: mailu-config
|
||||
ports:
|
||||
- name: smtp
|
||||
containerPort: 25
|
||||
- name: smtps
|
||||
containerPort: 465
|
||||
- name: submission
|
||||
containerPort: 587
|
||||
volumeMounts:
|
||||
- name: data
|
||||
subPath: mailqueue
|
||||
mountPath: /queue
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "2Gi"
|
||||
cpu: "1000m"
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: mailu-storage
|
||||
56
mailu/deployment-redis.yaml
Normal file
56
mailu/deployment-redis.yaml
Normal file
@@ -0,0 +1,56 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
namespace: {{ .namespace }}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
component: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
component: redis
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 999
|
||||
runAsGroup: 999
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: redis
|
||||
image: {{ .images.redis }}
|
||||
imagePullPolicy: IfNotPresent
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: [ALL]
|
||||
readOnlyRootFilesystem: false
|
||||
ports:
|
||||
- name: redis
|
||||
containerPort: 6379
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: 6379
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: 6379
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
volumes:
|
||||
- name: data
|
||||
emptyDir: {}
|
||||
45
mailu/deployment-rspamd.yaml
Normal file
45
mailu/deployment-rspamd.yaml
Normal file
@@ -0,0 +1,45 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: rspamd
|
||||
namespace: {{ .namespace }}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
component: rspamd
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
component: rspamd
|
||||
spec:
|
||||
containers:
|
||||
- name: rspamd
|
||||
image: {{ .images.rspamd }}
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: REDIS_ADDRESS
|
||||
value: "{{ .redis.host }}:{{ .redis.port }}"
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: mailu-config
|
||||
ports:
|
||||
- name: rspamd
|
||||
containerPort: 11332
|
||||
- name: http
|
||||
containerPort: 11334
|
||||
volumeMounts:
|
||||
- name: data
|
||||
subPath: rspamd
|
||||
mountPath: /var/lib/rspamd
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "2Gi"
|
||||
cpu: "2000m"
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: mailu-storage
|
||||
49
mailu/deployment-unbound.yaml
Normal file
49
mailu/deployment-unbound.yaml
Normal file
@@ -0,0 +1,49 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: unbound
|
||||
namespace: {{ .namespace }}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
component: unbound
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
component: unbound
|
||||
spec:
|
||||
containers:
|
||||
- name: unbound
|
||||
image: {{ .unbound.image }}
|
||||
imagePullPolicy: IfNotPresent
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: mailu-config
|
||||
env:
|
||||
- name: UNBOUND_TLS_NAME
|
||||
value: "dns"
|
||||
ports:
|
||||
- name: dns
|
||||
containerPort: 53
|
||||
protocol: UDP
|
||||
- name: dns-tcp
|
||||
containerPort: 53
|
||||
protocol: TCP
|
||||
resources:
|
||||
requests:
|
||||
memory: "128Mi"
|
||||
cpu: "50m"
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
cpu: "200m"
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: 53
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: 53
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
61
mailu/deployment-webmail.yaml
Normal file
61
mailu/deployment-webmail.yaml
Normal file
@@ -0,0 +1,61 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: webmail
|
||||
namespace: {{ .namespace }}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
component: webmail
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
component: webmail
|
||||
spec:
|
||||
initContainers:
|
||||
- name: fix-permissions
|
||||
image: busybox:latest
|
||||
command: ['sh', '-c', 'chown -R 999:999 /data']
|
||||
volumeMounts:
|
||||
- name: data
|
||||
subPath: webmail
|
||||
mountPath: /data
|
||||
containers:
|
||||
- name: webmail
|
||||
image: {{ .images.webmail }}
|
||||
imagePullPolicy: IfNotPresent
|
||||
securityContext:
|
||||
capabilities:
|
||||
add:
|
||||
- SYS_CHROOT
|
||||
- CHOWN
|
||||
- SETGID
|
||||
- SETUID
|
||||
env:
|
||||
- name: SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mailu-secrets
|
||||
key: secretKey
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: mailu-config
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
volumeMounts:
|
||||
- name: data
|
||||
subPath: webmail
|
||||
mountPath: /data
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "1000m"
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: mailu-storage
|
||||
42
mailu/ingress.yaml
Normal file
42
mailu/ingress.yaml
Normal file
@@ -0,0 +1,42 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: mailu
|
||||
namespace: {{ .namespace }}
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
external-dns.alpha.kubernetes.io/target: {{ .externalDnsDomain }}
|
||||
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ .hostname }}
|
||||
secretName: {{ .tlsSecretName }}
|
||||
rules:
|
||||
- host: {{ .hostname }}
|
||||
http:
|
||||
paths:
|
||||
- path: /admin
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: admin
|
||||
port:
|
||||
number: 80
|
||||
- path: /webmail
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: webmail
|
||||
port:
|
||||
number: 80
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: front
|
||||
port:
|
||||
number: 80
|
||||
25
mailu/kustomization.yaml
Normal file
25
mailu/kustomization.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: mailu
|
||||
labels:
|
||||
- includeSelectors: true
|
||||
pairs:
|
||||
app: mailu
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- pvc.yaml
|
||||
- configmap.yaml
|
||||
- deployment-redis.yaml
|
||||
- service-redis.yaml
|
||||
- deployment-unbound.yaml
|
||||
- service-unbound.yaml
|
||||
- deployment-admin.yaml
|
||||
- deployment-front.yaml
|
||||
- deployment-postfix.yaml
|
||||
- deployment-dovecot.yaml
|
||||
- deployment-rspamd.yaml
|
||||
- deployment-webmail.yaml
|
||||
- service.yaml
|
||||
- ingress.yaml
|
||||
60
mailu/manifest.yaml
Normal file
60
mailu/manifest.yaml
Normal file
@@ -0,0 +1,60 @@
|
||||
name: mailu
|
||||
is: mailu
|
||||
description: Mailu is a simple yet full-featured mail server as a set of Docker images. It includes a mail transfer agent, mail delivery agent, webmail, antispam, antivirus, and admin interface.
|
||||
version: 2024.06
|
||||
icon: https://mailu.io/master/_static/mailu_logo.svg
|
||||
defaultConfig:
|
||||
namespace: mailu
|
||||
|
||||
# Domain configuration
|
||||
domain: "{{ .cloud.baseDomain }}"
|
||||
hostname: mail.{{ .cloud.domain }}
|
||||
|
||||
# Container images (from ghcr.io)
|
||||
images:
|
||||
admin: ghcr.io/mailu/admin:2024.06
|
||||
front: ghcr.io/mailu/nginx:2024.06
|
||||
postfix: ghcr.io/mailu/postfix:2024.06
|
||||
dovecot: ghcr.io/mailu/dovecot:2024.06
|
||||
rspamd: ghcr.io/mailu/rspamd:2024.06
|
||||
clamav: ghcr.io/mailu/clamav:2024.06
|
||||
webmail: ghcr.io/mailu/webmail:2024.06
|
||||
redis: redis:alpine
|
||||
|
||||
# Redis configuration (built-in Redis without authentication)
|
||||
redis:
|
||||
host: redis.mailu.svc.cluster.local
|
||||
port: 6379
|
||||
|
||||
# Unbound DNS resolver (for DNSSEC validation)
|
||||
unbound:
|
||||
image: ghcr.io/mailu/unbound:2024.06
|
||||
ip: 10.96.200.1
|
||||
|
||||
# Timezone
|
||||
timezone: UTC
|
||||
|
||||
# Storage
|
||||
storage: 100Gi
|
||||
|
||||
# Initial admin account
|
||||
initialAccount:
|
||||
enabled: true
|
||||
username: admin
|
||||
domain: "{{ .cloud.baseDomain }}"
|
||||
email: "{{ .operator.email }}"
|
||||
|
||||
# TLS configuration
|
||||
tlsSecretName: mailu-tls
|
||||
externalDnsDomain: "{{ .cloud.domain }}"
|
||||
|
||||
# Log level
|
||||
logLevel: WARNING
|
||||
|
||||
# SMTP relay (optional)
|
||||
relayHost: ""
|
||||
relayPort: 25
|
||||
|
||||
defaultSecrets:
|
||||
- key: secretKey
|
||||
- key: initialAccountPassword
|
||||
@@ -1,4 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: cert-manager
|
||||
name: {{ .namespace }}
|
||||
@@ -1,12 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: docker-registry-pvc
|
||||
name: mailu-storage
|
||||
namespace: {{ .namespace }}
|
||||
spec:
|
||||
storageClassName: longhorn
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
volumeMode: Filesystem
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .storage }}
|
||||
14
mailu/service-redis.yaml
Normal file
14
mailu/service-redis.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis
|
||||
namespace: {{ .namespace }}
|
||||
spec:
|
||||
selector:
|
||||
component: redis
|
||||
ports:
|
||||
- name: redis
|
||||
port: 6379
|
||||
targetPort: 6379
|
||||
protocol: TCP
|
||||
type: ClusterIP
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user