this repo has no description

Add Opake deployment, wildcard cert, DNS01 solver, and routing cleanup

- Deploy Opake appview + web to k8s (new namespace, PVCs, ingress)
- Switch PDS cert to wildcard (*.sans-self.org) for subdomain handles
- Add DNS01 webhook solver (Hetzner) to ClusterIssuer for wildcard validation
- Add Opake DNS records (apex + appview A)
- Simplify PDS ingress: wildcard HostRegexp for subdomain handle resolution
- Add knot SSH key permissions init container

+317 -14
+20
dns.tf
··· 68 68 ttl = 3600 69 69 } 70 70 71 + resource "hcloud_zone_rrset" "opake_apex_a" { 72 + zone = hcloud_zone.opake.id 73 + name = "@" 74 + type = "A" 75 + ttl = 300 76 + records = [ 77 + { value = local.cluster_ip }, 78 + ] 79 + } 80 + 81 + resource "hcloud_zone_rrset" "opake_appview_a" { 82 + zone = hcloud_zone.opake.id 83 + name = "appview" 84 + type = "A" 85 + ttl = 300 86 + records = [ 87 + { value = local.cluster_ip }, 88 + ] 89 + } 90 + 71 91 resource "hcloud_zone_rrset" "opake_issues_a" { 72 92 zone = hcloud_zone.opake.id 73 93 name = "issues"
+8
k8s/knot/deployment.yaml
··· 16 16 app: knot 17 17 spec: 18 18 terminationGracePeriodSeconds: 30 19 + initContainers: 20 + - name: fix-ssh-perms 21 + image: busybox:1 22 + command: ["sh", "-c", "chmod 600 /etc/ssh/keys/* 2>/dev/null || true"] 23 + volumeMounts: 24 + - name: data 25 + mountPath: /etc/ssh/keys 26 + subPath: sshd-keys 19 27 containers: 20 28 - name: knot 21 29 image: tngl/knot:v1.10.0-alpha
+12
k8s/opake/appview/cert.yaml
··· 1 + apiVersion: cert-manager.io/v1 2 + kind: Certificate 3 + metadata: 4 + name: opake-appview 5 + namespace: opake 6 + spec: 7 + secretName: opake-appview-tls 8 + issuerRef: 9 + name: letsencrypt-prod 10 + kind: ClusterIssuer 11 + dnsNames: 12 + - appview.opake.app
+10
k8s/opake/appview/configmap.yaml
··· 1 + apiVersion: v1 2 + kind: ConfigMap 3 + metadata: 4 + name: opake-appview-config 5 + namespace: opake 6 + data: 7 + appview.toml: | 8 + jetstream_url = "wss://jetstream2.us-east.bsky.network/subscribe" 9 + listen = "0.0.0.0:6100" 10 + db_path = "/data/appview.db"
+79
k8s/opake/appview/deployment.yaml
··· 1 + apiVersion: apps/v1 2 + kind: Deployment 3 + metadata: 4 + name: opake-appview 5 + namespace: opake 6 + spec: 7 + replicas: 1 8 + strategy: 9 + type: Recreate # SQLite WAL — one writer 10 + selector: 11 + matchLabels: 12 + app: opake-appview 13 + template: 14 + metadata: 15 + labels: 16 + app: opake-appview 17 + spec: 18 + terminationGracePeriodSeconds: 30 19 + securityContext: 20 + fsGroup: 1000 21 + runAsUser: 1000 22 + runAsGroup: 1000 23 + runAsNonRoot: true 24 + containers: 25 + - name: appview 26 + image: zot.sans-self.org/opake/appview:REPLACE_ME 27 + ports: 28 + - containerPort: 6100 29 + env: 30 + - name: OPAKE_DATA_DIR 31 + value: /data 32 + - name: RUST_LOG 33 + value: info 34 + volumeMounts: 35 + - name: data 36 + mountPath: /data 37 + - name: config 38 + mountPath: /data/appview.toml 39 + subPath: appview.toml 40 + readOnly: true 41 + startupProbe: 42 + httpGet: 43 + path: /api/health 44 + port: 6100 45 + failureThreshold: 30 46 + periodSeconds: 2 47 + livenessProbe: 48 + httpGet: 49 + path: /api/health 50 + port: 6100 51 + periodSeconds: 30 52 + timeoutSeconds: 5 53 + failureThreshold: 3 54 + readinessProbe: 55 + httpGet: 56 + path: /api/health 57 + port: 6100 58 + periodSeconds: 10 59 + timeoutSeconds: 5 60 + failureThreshold: 2 61 + securityContext: 62 + allowPrivilegeEscalation: false 63 + capabilities: 64 + drop: 65 + - ALL 66 + resources: 67 + requests: 68 + cpu: 50m 69 + memory: 64Mi 70 + limits: 71 + cpu: 500m 72 + memory: 256Mi 73 + volumes: 74 + - name: data 75 + persistentVolumeClaim: 76 + claimName: opake-appview-data 77 + - name: config 78 + configMap: 79 + name: opake-appview-config
+16
k8s/opake/appview/ingress.yaml
··· 1 + apiVersion: traefik.io/v1alpha1 2 + kind: IngressRoute 3 + metadata: 4 + name: opake-appview 5 + namespace: opake 6 + spec: 7 + entryPoints: 8 + - websecure 9 + tls: 10 + secretName: opake-appview-tls 11 + routes: 12 + - match: Host(`appview.opake.app`) 13 + kind: Rule 14 + services: 15 + - name: opake-appview 16 + port: 6100
+14
k8s/opake/appview/kustomization.yaml
··· 1 + apiVersion: kustomize.config.k8s.io/v1beta1 2 + kind: Kustomization 3 + 4 + resources: 5 + - configmap.yaml 6 + - pvc.yaml 7 + - deployment.yaml 8 + - service.yaml 9 + - ingress.yaml 10 + - cert.yaml 11 + 12 + images: 13 + - name: zot.sans-self.org/opake/appview 14 + newTag: latest
+11
k8s/opake/appview/pvc.yaml
··· 1 + apiVersion: v1 2 + kind: PersistentVolumeClaim 3 + metadata: 4 + name: opake-appview-data 5 + namespace: opake 6 + spec: 7 + accessModes: 8 + - ReadWriteOnce 9 + resources: 10 + requests: 11 + storage: 1Gi
+12
k8s/opake/appview/service.yaml
··· 1 + apiVersion: v1 2 + kind: Service 3 + metadata: 4 + name: opake-appview 5 + namespace: opake 6 + spec: 7 + selector: 8 + app: opake-appview 9 + ports: 10 + - name: http 11 + port: 6100 12 + targetPort: 6100
+7
k8s/opake/kustomization.yaml
··· 1 + apiVersion: kustomize.config.k8s.io/v1beta1 2 + kind: Kustomization 3 + 4 + resources: 5 + - namespace.yaml 6 + - web 7 + - appview
+4
k8s/opake/namespace.yaml
··· 1 + apiVersion: v1 2 + kind: Namespace 3 + metadata: 4 + name: opake
+12
k8s/opake/web/cert.yaml
··· 1 + apiVersion: cert-manager.io/v1 2 + kind: Certificate 3 + metadata: 4 + name: opake-web 5 + namespace: opake 6 + spec: 7 + secretName: opake-app-tls 8 + issuerRef: 9 + name: letsencrypt-prod 10 + kind: ClusterIssuer 11 + dnsNames: 12 + - opake.app
+60
k8s/opake/web/deployment.yaml
··· 1 + apiVersion: apps/v1 2 + kind: Deployment 3 + metadata: 4 + name: opake-web 5 + namespace: opake 6 + spec: 7 + replicas: 2 8 + selector: 9 + matchLabels: 10 + app: opake-web 11 + template: 12 + metadata: 13 + labels: 14 + app: opake-web 15 + spec: 16 + securityContext: 17 + runAsUser: 1000 18 + runAsGroup: 1000 19 + runAsNonRoot: true 20 + containers: 21 + - name: web 22 + image: zot.sans-self.org/opake/web:REPLACE_ME 23 + ports: 24 + - containerPort: 3000 25 + env: 26 + - name: NODE_ENV 27 + value: production 28 + - name: PORT 29 + value: "3000" 30 + livenessProbe: 31 + httpGet: 32 + path: / 33 + port: 3000 34 + periodSeconds: 30 35 + failureThreshold: 3 36 + readinessProbe: 37 + httpGet: 38 + path: / 39 + port: 3000 40 + periodSeconds: 10 41 + failureThreshold: 2 42 + securityContext: 43 + allowPrivilegeEscalation: false 44 + capabilities: 45 + drop: 46 + - ALL 47 + readOnlyRootFilesystem: true 48 + resources: 49 + requests: 50 + cpu: 20m 51 + memory: 64Mi 52 + limits: 53 + cpu: 200m 54 + memory: 256Mi 55 + volumeMounts: 56 + - name: tmp 57 + mountPath: /tmp 58 + volumes: 59 + - name: tmp 60 + emptyDir: {}
+16
k8s/opake/web/ingress.yaml
··· 1 + apiVersion: traefik.io/v1alpha1 2 + kind: IngressRoute 3 + metadata: 4 + name: opake-web 5 + namespace: opake 6 + spec: 7 + entryPoints: 8 + - websecure 9 + tls: 10 + secretName: opake-app-tls 11 + routes: 12 + - match: Host(`opake.app`) 13 + kind: Rule 14 + services: 15 + - name: opake-web 16 + port: 3000
+12
k8s/opake/web/kustomization.yaml
··· 1 + apiVersion: kustomize.config.k8s.io/v1beta1 2 + kind: Kustomization 3 + 4 + resources: 5 + - deployment.yaml 6 + - service.yaml 7 + - ingress.yaml 8 + - cert.yaml 9 + 10 + images: 11 + - name: zot.sans-self.org/opake/web 12 + newTag: latest
+12
k8s/opake/web/service.yaml
··· 1 + apiVersion: v1 2 + kind: Service 3 + metadata: 4 + name: opake-web 5 + namespace: opake 6 + spec: 7 + selector: 8 + app: opake-web 9 + ports: 10 + - name: http 11 + port: 3000 12 + targetPort: 3000
+1 -3
k8s/pds/cert.yaml
··· 10 10 kind: ClusterIssuer 11 11 dnsNames: 12 12 - sans-self.org 13 - - pds-next.sans-self.org 14 - - vesper.sans-self.org 15 - - nyx.sans-self.org 13 + - "*.sans-self.org"
+3 -11
k8s/pds/ingress.yaml
··· 36 36 services: 37 37 - name: tarpit 38 38 port: 8080 39 - # Admin and account creation — blocked externally 40 - - match: Host(`sans-self.org`) && (PathPrefix(`/xrpc/com.atproto.admin`) || PathPrefix(`/xrpc/com.atproto.server.createAccount`)) 41 - kind: Rule 42 - priority: 100 43 - middlewares: 44 - - name: block-external 45 - services: 46 - - name: tranquil-pds 47 - port: 3000 48 - # OAuth client metadata — static file served by frontend (with hostname substitution) 39 + # OAuth client metadata — static file served by frontend (with hostname substitution) 49 40 - match: Host(`sans-self.org`) && Path(`/oauth/client-metadata.json`) 50 41 kind: Rule 51 42 priority: 60 ··· 72 63 - name: tranquil-pds 73 64 port: 3000 74 65 # Subdomain handle resolution (well-known + xrpc) 75 - - match: Host(`pds-test.sans-self.org`) || Host(`vesper.sans-self.org`) || Host(`nyx.sans-self.org`) 66 + - match: HostRegexp(`[a-z0-9-]+\.sans-self\.org`) 76 67 kind: Rule 68 + priority: 1 77 69 middlewares: 78 70 - name: strip-server-headers 79 71 services:
k8s/pds/tranquil-valkey-url.secret

This is a binary file and will not be displayed.

+8
k8s/shared/cluster-issuer.yaml
··· 12 12 - http01: 13 13 ingress: 14 14 ingressClassName: traefik 15 + - dns01: 16 + webhook: 17 + groupName: acme.hetzner.com 18 + solverName: hetzner 19 + config: 20 + tokenSecretKeyRef: 21 + name: hetzner-api-token 22 + key: token
k8s/shared/hetzner-dns-token.secret

This is a binary file and will not be displayed.