@yunuseon/kayra 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -2
  3. package/ansible/.ansible-lint +13 -0
  4. package/ansible/ansible.cfg +12 -0
  5. package/ansible/playbooks/adopt.yml +35 -0
  6. package/ansible/playbooks/app.yml +21 -0
  7. package/ansible/playbooks/apps-prune.yml +30 -0
  8. package/ansible/playbooks/edge.yml +41 -0
  9. package/ansible/playbooks/nas.yml +47 -0
  10. package/ansible/playbooks/platform.yml +62 -0
  11. package/ansible/requirements.yml +7 -0
  12. package/ansible/roles/app/defaults/main.yml +9 -0
  13. package/ansible/roles/app/tasks/main.yml +107 -0
  14. package/ansible/roles/app/templates/app.env.j2 +9 -0
  15. package/ansible/roles/app/templates/casc-app.yaml.j2 +68 -0
  16. package/ansible/roles/backup-check/files/backup-check.service +9 -0
  17. package/ansible/roles/backup-check/files/backup-check.timer +10 -0
  18. package/ansible/roles/backup-check/tasks/main.yml +59 -0
  19. package/ansible/roles/backup-check/templates/backup-check.sh.j2 +117 -0
  20. package/ansible/roles/common/handlers/main.yml +16 -0
  21. package/ansible/roles/common/tasks/main.yml +100 -0
  22. package/ansible/roles/compose-stacks/tasks/main.yml +74 -0
  23. package/ansible/roles/docker/defaults/main.yml +3 -0
  24. package/ansible/roles/docker/files/buildkit-rootless +23 -0
  25. package/ansible/roles/docker/handlers/main.yml +12 -0
  26. package/ansible/roles/docker/tasks/main.yml +82 -0
  27. package/ansible/roles/edge-proxy/files/docker-compose.yml +13 -0
  28. package/ansible/roles/edge-proxy/handlers/main.yml +6 -0
  29. package/ansible/roles/edge-proxy/tasks/main.yml +34 -0
  30. package/ansible/roles/edge-proxy/templates/Caddyfile.j2 +25 -0
  31. package/ansible/roles/forgejo-config/tasks/main.yml +292 -0
  32. package/ansible/roles/platform/defaults/main.yml +76 -0
  33. package/ansible/roles/platform/tasks/main.yml +298 -0
  34. package/ansible/roles/platform/templates/authelia-users.yml.j2 +17 -0
  35. package/ansible/roles/platform/templates/platform.env.j2 +41 -0
  36. package/ansible/roles/restic-backup/files/docker-compose.postgres.yml +27 -0
  37. package/ansible/roles/restic-backup/files/docker-compose.yml +12 -0
  38. package/ansible/roles/restic-backup/handlers/main.yml +7 -0
  39. package/ansible/roles/restic-backup/tasks/main.yml +121 -0
  40. package/ansible/roles/restic-backup/templates/backup.env.j2 +9 -0
  41. package/ansible/roles/restic-backup/templates/backup.sh.j2 +28 -0
  42. package/ansible/roles/restic-backup/templates/docker-compose.override.yml.j2 +26 -0
  43. package/ansible/roles/restic-backup/templates/follow-restore-test.sh.j2 +28 -0
  44. package/ansible/roles/restic-backup/templates/restore-test.sh.j2 +51 -0
  45. package/ansible/roles/restic-backup/templates/root.crontab.j2 +6 -0
  46. package/ansible/roles/restic-backup/templates/ssh_config.j2 +12 -0
  47. package/ansible/roles/wireguard/handlers/main.yml +9 -0
  48. package/ansible/roles/wireguard/tasks/main.yml +33 -0
  49. package/ansible/roles/wireguard/templates/wg0.conf.j2 +12 -0
  50. package/bin/kayra.js +29 -0
  51. package/cli/dist/brand.js +41 -0
  52. package/cli/dist/cli.js +21 -0
  53. package/cli/dist/commands/doctor.js +79 -0
  54. package/cli/dist/commands/server.js +215 -0
  55. package/cli/dist/commands/status.js +43 -0
  56. package/cli/dist/fleet.js +134 -0
  57. package/cli/dist/interpret.js +98 -0
  58. package/cli/dist/load.js +49 -0
  59. package/cli/dist/main.js +94 -0
  60. package/cli/dist/plan.js +31 -0
  61. package/cli/dist/result.js +5 -0
  62. package/cloud-init.yml.tpl +14 -0
  63. package/compose/authelia/configuration.yml +138 -0
  64. package/compose/buildkit/buildkitd.toml +14 -0
  65. package/compose/docker-compose.acme-dns.yml +12 -0
  66. package/compose/docker-compose.ci.yml +156 -0
  67. package/compose/docker-compose.yml +83 -0
  68. package/compose/jenkins/Dockerfile +40 -0
  69. package/compose/jenkins/casc/00-platform.yaml +158 -0
  70. package/compose/jenkins/plugins.txt +97 -0
  71. package/compose/jenkins-agent/Dockerfile +24 -0
  72. package/compose/traefik/dynamic/authelia.yml +18 -0
  73. package/compose/traefik/dynamic/middlewares.yml +14 -0
  74. package/compose/traefik/dynamic/ratelimit.yml +21 -0
  75. package/compose/traefik/dynamic/tls.yml +9 -0
  76. package/compose/traefik/traefik.yml +46 -0
  77. package/hosts.conf +6 -0
  78. package/package.json +73 -4
  79. package/scripts/local/app-manifests.sh +26 -0
  80. package/scripts/local/authelia-dev-init.sh +58 -0
  81. package/scripts/local/certs.sh +55 -0
  82. package/scripts/local/home-image.sh +55 -0
  83. package/scripts/local/home-reach.sh +41 -0
  84. package/scripts/local/require-main.sh +38 -0
  85. package/scripts/local/setup-hosts.sh +30 -0
  86. package/scripts/server/app-purge +26 -0
  87. package/scripts/server/app-remove +30 -0
  88. package/scripts/server/jenkins-api +28 -0
  89. package/scripts/server/jenkins-app-apply +36 -0
  90. package/scripts/server/jenkins-reload +12 -0
  91. package/terraform/modules/hcloud-firewall/main.tf +54 -0
  92. package/terraform/modules/hcloud-firewall/outputs.tf +9 -0
  93. package/terraform/modules/hcloud-firewall/variables.tf +22 -0
  94. package/terraform/modules/hcloud-server/main.tf +62 -0
  95. package/terraform/modules/hcloud-server/outputs.tf +19 -0
  96. package/terraform/modules/hcloud-server/templates/inventory.ini.tpl +5 -0
  97. package/terraform/modules/hcloud-server/variables.tf +50 -0
  98. package/terraform/modules/multipass-instance/main.tf +67 -0
  99. package/terraform/modules/multipass-instance/outputs.tf +19 -0
  100. package/terraform/modules/multipass-instance/templates/inventory.ini.tpl +5 -0
  101. package/terraform/modules/multipass-instance/variables.tf +39 -0
@@ -0,0 +1,97 @@
1
+ # Every plugin, dependencies included, at the version prod runs: without the
2
+ # dependencies pinned, jenkins-plugin-cli resolves them to whatever is newest
3
+ # on the day the image is built. Renovate proposes updates.
4
+ antisamy-markup-formatter:173.v680e3a_b_69ff3
5
+ apache-httpcomponents-client-4-api:4.5.14-269.vfa_2321039a_83
6
+ apache-httpcomponents-client-5-api:5.6.4-207.vd5b_a_c15e4de5
7
+ asm-api:9.10.1-216.va_9256d3b_844b_
8
+ authentication-tokens:1.144.v5ff4a_5ec5c33
9
+ bootstrap5-api:5.3.8-1048.va_c299057e35c
10
+ bouncycastle-api:2.30.1.86.1-306.vfc711fa_36d5f
11
+ branch-api:2.1303.v9f3b_95dc329d
12
+ caffeine-api:3.3.0-210.v71b_2985ce530
13
+ checks-api:422.vc7e8b_51488c6
14
+ cloud-stats:423.v63f017a_f582c
15
+ cloudbees-folder:6.1106.v3a_d9a_6d2465e
16
+ commons-compress-api:1.28.0-95.vb_e5e24f83645
17
+ commons-lang3-api:3.21.0-122.v6a_ea_4fd34049
18
+ commons-text-api:1.15.0-218.va_61573470393
19
+ configuration-as-code:2133.ve570217cdb_cd
20
+ credentials-binding:728.v902a_273b_8947
21
+ credentials:1511.v2e3cb_0008ef0
22
+ display-url-api:2.217.va_6b_de84cc74b_
23
+ docker-commons:477.v289085a_b_6896
24
+ docker-java-api:3.7.1-136.v5d70f77a_c3d6
25
+ docker-plugin:1331.v2c036a_dc63fc
26
+ docker-workflow:653.v2f2c08eff0ec
27
+ durable-task:686.v80ff80875b_82
28
+ echarts-api:6.1.0-1321.v86c4c5114494
29
+ eddsa-api:0.3.0.1-29.v67e9a_1c969b_b_
30
+ font-awesome-api:7.3.1-1013.v0835a_879ec6d
31
+ git-client:6.6.1
32
+ git:5.10.1
33
+ github-api:1.330-492.v3941a_032db_2a_
34
+ github:1.47.0
35
+ gson-api:2.14.0-201.v8eefe5515533
36
+ instance-identity:203.v15e81a_1b_7a_38
37
+ ionicons-api:94.vcc3065403257
38
+ jackson-annotations2-api:2.22-19.v10a_a_582ea_26e
39
+ jackson2-api:2.22.3-447.v6a_93ce7d2c82
40
+ jackson3-api:3.2.3-101.v69fc84453a_8e
41
+ jakarta-activation-api:2.1.4-1
42
+ jakarta-mail-api:2.1.5-1
43
+ jakarta-xml-bind-api:4.0.9-19.v2b_a_5b_44d9a_1c
44
+ javax-activation-api:1.2.0-8
45
+ jaxb:2.3.9-143.v5979df3304e6
46
+ job-dsl:3732.v9a_c49a_61a_313
47
+ joda-time-api:2.15.0-208.v6b_084e7a_d9f0
48
+ jquery3-api:3.7.1-687.v68d468e40b_30
49
+ json-api:20260814-226.v20f9685d642c
50
+ json-path-api:3.0.0-218.vcd4dd1355de2
51
+ junit:1434.vde2d24df0c4c
52
+ mailer:534.v1b_36f5864073
53
+ matrix-project:928.v1effc3f3a_236
54
+ mina-sshd-api-common:2.19.0-192.v2b_a_7b_2c1dc71
55
+ mina-sshd-api-core:2.19.0-192.v2b_a_7b_2c1dc71
56
+ okhttp-api:5.3.2-200.vedb_720a_cf1f8
57
+ pipeline-build-step:601.v6d4c6d1a_9dc7
58
+ pipeline-graph-analysis:254.v0f63a_a_447dca_
59
+ pipeline-graph-view:1053.v9df19e6ceed2
60
+ pipeline-groovy-lib:806.v408277b_33d1d
61
+ pipeline-input-step:560.v56198a_642157
62
+ pipeline-milestone-step:152.v6e22b_8cfc66c
63
+ pipeline-model-api:2.2293.v6e7193cec599
64
+ pipeline-model-definition:2.2293.v6e7193cec599
65
+ pipeline-model-extensions:2.2293.v6e7193cec599
66
+ pipeline-rest-api:2.41
67
+ pipeline-stage-step:345.va_96187909426
68
+ pipeline-stage-tags-metadata:2.2293.v6e7193cec599
69
+ pipeline-stage-view:2.41
70
+ pipeline-utility-steps:3.810.va_7672d206740
71
+ plain-credentials:199.v9f8e1f741799
72
+ plugin-util-api:7.1341.v039f146993d9
73
+ prism-api:1.30.0-741.v034eb_0b_0a_a_fa_
74
+ resource-disposer:0.25
75
+ scm-api:728.vc30dcf7a_0df5
76
+ script-security:1433.v17c58a_e45cc1
77
+ snakeyaml-api:2.7-152.vc672728cf536
78
+ snakeyaml-engine-api:3.2-18.v885ed7ff698f
79
+ ssh-credentials:372.va_250881b_08cd
80
+ ssh-slaves:3.1097.v868116049892
81
+ structs:362.va_b_695ef4fdf9
82
+ timestamper:1.30
83
+ token-macro:477.vd4f0dc3cb_cf1
84
+ trilead-api:2.284.v1974ea_324382
85
+ variant:70.va_d9f17f859e0
86
+ woodstox-core-api:7.3.0-16.v017f30ed004c
87
+ workflow-aggregator:608.v67378e9d3db_1
88
+ workflow-api:1413.v2ff1a_5e720fa_
89
+ workflow-basic-steps:1098.v808b_fd7f8cf4
90
+ workflow-cps:4391.v652b_b_b_1b_7c00
91
+ workflow-durable-task-step:1479.v56e587f413a_7
92
+ workflow-job:1609.v8926e7962024
93
+ workflow-multibranch:842.v3a_b_59b_57b_e6e
94
+ workflow-scm-step:487.v41313a_96b_65d
95
+ workflow-step-api:724.v538c2362b_dfb_
96
+ workflow-support:1015.v785e5a_b_b_8b_22
97
+ ws-cleanup:0.49
@@ -0,0 +1,24 @@
1
+ # =============================================================================
2
+ # Jenkins Inbound Agent — git + buildctl + jq, nothing app-specific.
3
+ #
4
+ # Deliberately has NO docker CLI and no docker socket: it runs code from
5
+ # arbitrary branches, and the host's socket would make that host root. Images
6
+ # are built by buildkitd over TCP. No language toolchain either: apps build and
7
+ # test inside their Dockerfiles, so a new app never changes this image.
8
+ # =============================================================================
9
+
10
+ FROM jenkins/inbound-agent:3386.v353e57a_1b_ea_0-1-jdk21
11
+
12
+ USER root
13
+
14
+ # renovate: datasource=github-releases depName=moby/buildkit
15
+ ARG BUILDKIT_VERSION=v0.34.0
16
+
17
+ RUN apt-get update && \
18
+ apt-get install -y --no-install-recommends ca-certificates curl jq && \
19
+ arch="$(dpkg --print-architecture)" && \
20
+ curl -fsSL "https://github.com/moby/buildkit/releases/download/${BUILDKIT_VERSION}/buildkit-${BUILDKIT_VERSION}.linux-${arch}.tar.gz" \
21
+ | tar -xz -C /usr/local bin/buildctl && \
22
+ rm -rf /var/lib/apt/lists/*
23
+
24
+ USER jenkins
@@ -0,0 +1,18 @@
1
+ # Dynamic Traefik configuration — Authelia forward authentication.
2
+ # Referenced from router labels as `authelia@file`.
3
+ http:
4
+ middlewares:
5
+ authelia:
6
+ forwardAuth:
7
+ address: 'http://authelia:9091/api/authz/forward-auth'
8
+ # Safe because neither entrypoint in traefik.yml sets
9
+ # forwardedHeaders.trustedIPs, so Traefik overwrites any client-supplied
10
+ # X-Forwarded-* before this middleware runs. Do not add trustedIPs.
11
+ trustForwardHeader: true
12
+ # The regex form strips every matching header from the inbound request
13
+ # before copying Authelia's back, closing the gap where Authelia
14
+ # returns no value for one of them and a client-supplied one survives.
15
+ authResponseHeadersRegex: '^Remote-(User|Groups|Email|Name)$'
16
+ # Traefik warns on every request without this. Authelia's auth
17
+ # responses are small; an unbounded body is a memory-exhaustion vector.
18
+ maxResponseBodySize: 8192
@@ -0,0 +1,14 @@
1
+ # Dynamic Traefik configuration — middlewares
2
+ http:
3
+ middlewares:
4
+ # Security headers for all services
5
+ security-headers:
6
+ headers:
7
+ browserXssFilter: true
8
+ contentTypeNosniff: true
9
+ frameDeny: true
10
+ sslRedirect: false
11
+ customFrameOptionsValue: "SAMEORIGIN"
12
+ stsSeconds: 31536000
13
+ stsIncludeSubdomains: true
14
+ referrerPolicy: "strict-origin-when-cross-origin"
@@ -0,0 +1,21 @@
1
+ # Dynamic Traefik configuration — rate limiting.
2
+ http:
3
+ middlewares:
4
+ # The trial-request endpoint is public and unauthenticated by design: it is
5
+ # how strangers reach the club. That also makes it the one spam vector in
6
+ # the system, so it gets a ceiling.
7
+ #
8
+ # No sourceCriterion: the default keys on the client's remote address,
9
+ # which is correct when Traefik is the edge. An ipStrategy.depth here would
10
+ # expect an X-Forwarded-For chain that does not exist for a direct client,
11
+ # and the limit silently never engages.
12
+ #
13
+ # Values are empirical, not arithmetic: shorter periods did not actually
14
+ # throttle in testing, this regime does. 20 per 10 minutes with a burst of
15
+ # 5 is far more than a human filling in a form once, and caps a spammer at
16
+ # roughly 3k/day.
17
+ trial-ratelimit:
18
+ rateLimit:
19
+ average: 20
20
+ period: "10m"
21
+ burst: 5
@@ -0,0 +1,9 @@
1
+ # Dynamic TLS — file-based certificates for local + VM development.
2
+ # Generated by `make certs [VM_DOMAIN=...]` (mkcert under the hood).
3
+ # In prod, this directory is empty and ACME (Let's Encrypt) handles certs
4
+ # automatically. Traefik logs a warning for missing files but routes still
5
+ # work via the entrypoint's default certResolver.
6
+ tls:
7
+ certificates:
8
+ - certFile: /certs/cert.pem
9
+ keyFile: /certs/key.pem
@@ -0,0 +1,46 @@
1
+ # Traefik static configuration
2
+ api:
3
+ dashboard: true
4
+ # No insecure listener: the dashboard is served through a normal router on
5
+ # traefik.<DOMAIN>, behind Authelia. See docker-compose.yml.
6
+ insecure: false
7
+
8
+ entryPoints:
9
+ web:
10
+ address: ":80"
11
+ http:
12
+ redirections:
13
+ entryPoint:
14
+ to: websecure
15
+ scheme: https
16
+ permanent: true
17
+ websecure:
18
+ address: ":443"
19
+ http:
20
+ tls:
21
+ certResolver: le
22
+ # Every router, apps included: HSTS on half the hosts protects nothing.
23
+ middlewares:
24
+ - security-headers@file
25
+
26
+ certificatesResolvers:
27
+ le:
28
+ acme:
29
+ # Traefik does not expand env vars in its static file; the platform role
30
+ # substitutes acme_email on copy. Local dev never registers.
31
+ email: "acme@example.com"
32
+ storage: /acme/acme.json
33
+ httpChallenge:
34
+ entryPoint: web
35
+
36
+ providers:
37
+ docker:
38
+ endpoint: "unix:///var/run/docker.sock"
39
+ exposedByDefault: false
40
+ network: platform-public
41
+ file:
42
+ directory: /etc/traefik/dynamic
43
+ watch: true
44
+
45
+ log:
46
+ level: INFO
package/hosts.conf ADDED
@@ -0,0 +1,6 @@
1
+ # Platform subdomains to add to /etc/hosts and to the mkcert SAN list.
2
+ # App subdomains live in each app's app.yml; apps may not claim these.
3
+ # One subdomain per line. Read by infrastructure/scripts/local/setup-hosts.sh and infrastructure/scripts/local/certs.sh.
4
+ jenkins
5
+ auth
6
+ traefik
package/package.json CHANGED
@@ -1,6 +1,75 @@
1
1
  {
2
2
  "name": "@yunuseon/kayra",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.4.0",
4
+ "description": "A self-hosted app platform and the CLI that runs it: Traefik, Authelia, Jenkins, a registry and rootless BuildKit per server, set up with OpenTofu and Ansible.",
5
+ "license": "MIT",
6
+ "author": "Yunus Öztürk",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/yunuseon/kayra.git"
10
+ },
11
+ "type": "module",
12
+ "engines": {
13
+ "node": ">=24"
14
+ },
15
+ "bin": {
16
+ "kayra": "bin/kayra.js"
17
+ },
18
+ "files": [
19
+ "bin/",
20
+ "cli/dist/",
21
+ "ansible/",
22
+ "compose/",
23
+ "terraform/",
24
+ "scripts/",
25
+ "cloud-init.yml.tpl",
26
+ "hosts.conf"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public",
30
+ "provenance": true
31
+ },
32
+ "scripts": {
33
+ "kayra": "node cli/src/main.ts",
34
+ "build": "tsc -p cli/tsconfig.build.json",
35
+ "prepack": "npm run build",
36
+ "typecheck": "tsc -p cli --noEmit",
37
+ "lint": "cd cli && eslint --flag unstable_native_nodejs_ts_config .",
38
+ "test": "node --test 'cli/test/**/*.test.ts'"
39
+ },
40
+ "dependencies": {
41
+ "@optique/core": "1.3.3",
42
+ "@optique/run": "1.3.3",
43
+ "yaml": "2.9.1"
44
+ },
45
+ "devDependencies": {
46
+ "@eslint/js": "10.0.1",
47
+ "@tsconfig/node24": "24.0.5",
48
+ "@tsconfig/strictest": "2.0.8",
49
+ "@types/node": "24.19.1",
50
+ "eslint": "10.12.0",
51
+ "eslint-plugin-functional": "10.0.1",
52
+ "typescript": "6.0.3",
53
+ "typescript-eslint": "8.71.1"
54
+ },
55
+ "overrides": {
56
+ "eslint-plugin-functional": {
57
+ "deepmerge-ts": "8.0.2"
58
+ }
59
+ },
60
+ "homepage": "https://github.com/yunuseon/kayra#readme",
61
+ "bugs": {
62
+ "url": "https://github.com/yunuseon/kayra/issues"
63
+ },
64
+ "keywords": [
65
+ "paas",
66
+ "self-hosted",
67
+ "platform",
68
+ "traefik",
69
+ "jenkins",
70
+ "ansible",
71
+ "opentofu",
72
+ "hetzner",
73
+ "docker-compose"
74
+ ]
75
+ }
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+ # Print the path of every app.yml a server runs, one per line, from the
3
+ # platform's cache of each app's repo (.cache/apps/<name>, the same clone the
4
+ # app role reads). A missing clone is
5
+ # fetched with the operator's own git credentials (kayra doctor checks them).
6
+ #
7
+ # Usage: FLEET_ROOT=<fleet> bash scripts/local/app-manifests.sh [--subdomains] <server-name>
8
+ # --subdomains print the apps' subdomains instead (for certs and /etc/hosts)
9
+ set -euo pipefail
10
+
11
+ FLEET_ROOT="${FLEET_ROOT:?the directory of the fleet (servers/), set by kayra}"
12
+ subdomains=false
13
+ [ "${1:-}" != "--subdomains" ] || { subdomains=true; shift; }
14
+ name="${1:?Usage: app-manifests.sh [--subdomains] <server-name>}"
15
+ descriptor="${FLEET_ROOT}/servers/${name}/server.yml"
16
+
17
+ yq -o json -I0 '.apps // [] | .[]' "$descriptor" | while read -r app; do
18
+ path="$(jq -r '.path // "."' <<< "$app")"
19
+ repo="$(jq -r '.repo' <<< "$app")"
20
+ clone="${FLEET_ROOT}/.cache/apps/$(jq -r '.name' <<< "$app")"
21
+ if [ ! -d "${clone}/.git" ]; then
22
+ git clone -q --depth 1 --branch "$(jq -r '.ref // "main"' <<< "$app")" "$repo" "$clone"
23
+ fi
24
+ manifest="${clone}/${path}/app.yml"
25
+ if [ "$subdomains" = true ]; then yq '.subdomains[]' "$manifest"; else echo "$manifest"; fi
26
+ done
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env bash
2
+ # Generate local Authelia secrets and a dev user database.
3
+ #
4
+ # Mirrors scripts/local/certs.sh: local-only material, generated on demand and
5
+ # gitignored, never committed. Production gets both from Ansible + vault.
6
+ #
7
+ # Usage: bash scripts/local/authelia-dev-init.sh <authelia-dir> <dev-email> <dev-password>
8
+ set -euo pipefail
9
+
10
+ usage="Usage: authelia-dev-init.sh <authelia-dir> <dev-email> <dev-password>"
11
+ AUTHELIA_DIR="${1:?$usage}"
12
+ DEV_EMAIL="${2:?$usage}"
13
+ DEV_PASSWORD="${3:?$usage}"
14
+ IMAGE="authelia/authelia:4.39.28"
15
+
16
+ SECRETS_DIR="${AUTHELIA_DIR}/secrets"
17
+ USERS_FILE="${AUTHELIA_DIR}/users_database.yml"
18
+
19
+ mkdir -p "${SECRETS_DIR}"
20
+
21
+ for name in session storage_encryption; do
22
+ target="${SECRETS_DIR}/${name}"
23
+ if [ ! -s "${target}" ]; then
24
+ echo "==> Generating ${name}..."
25
+ openssl rand -hex 32 > "${target}"
26
+ chmod 600 "${target}"
27
+ fi
28
+ done
29
+
30
+ if [ -s "${USERS_FILE}" ]; then
31
+ echo "==> ${USERS_FILE} exists — leaving it alone."
32
+ exit 0
33
+ fi
34
+
35
+ echo "==> Hashing the dev password (argon2id)..."
36
+ # Authelia's own hasher, so parameters always match configuration.yml.
37
+ # --memory is KiB here, matching the config's `memory:` key exactly.
38
+ HASH="$(docker run --rm "${IMAGE}" \
39
+ authelia crypto hash generate argon2 \
40
+ --variant argon2id --iterations 3 --memory 32768 --parallelism 2 \
41
+ --password "${DEV_PASSWORD}" \
42
+ | sed 's/^Digest: //')"
43
+
44
+ cat > "${USERS_FILE}" <<YML
45
+ # Local development only — generated by scripts/local/authelia-dev-init.sh.
46
+ # Production renders this from vault; see the platform Ansible role.
47
+ users:
48
+ dev:
49
+ disabled: false
50
+ displayname: 'Dev'
51
+ password: '${HASH}'
52
+ email: '${DEV_EMAIL}'
53
+ groups: ['admin']
54
+ YML
55
+ chmod 600 "${USERS_FILE}"
56
+
57
+ echo ""
58
+ echo "Done — dev login: ${DEV_EMAIL} / ${DEV_PASSWORD}"
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env bash
2
+ # Generate local-trusted TLS certs (mkcert) for Traefik.
3
+ # Covers `<domain>`, `*.<domain>`, the platform's subdomains and the given ones.
4
+ #
5
+ # Usage: bash scripts/local/certs.sh <domain> <certs-dir> [subdomain…]
6
+ # Output: <certs-dir>/cert.pem, <certs-dir>/key.pem
7
+ #
8
+ # A no-op when the existing cert already covers <domain>. Local dev and the
9
+ # multipass VM share one certs dir with different domains, so "the file exists"
10
+ # is not enough: shipping the local cert to the VM would break every hostname.
11
+ set -euo pipefail
12
+
13
+ KAYRA_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
14
+ DOMAIN="${1:?Usage: certs.sh <domain> <certs-dir> [subdomain…]}"
15
+ CERTS_DIR="${2:?}"
16
+ shift 2
17
+
18
+ if [ -f "${CERTS_DIR}/key.pem" ] && [ -f "${CERTS_DIR}/cert.pem" ] \
19
+ && openssl x509 -in "${CERTS_DIR}/cert.pem" -noout -ext subjectAltName | grep -qE "DNS:${DOMAIN}(,|$)"; then
20
+ exit 0
21
+ fi
22
+
23
+ if ! command -v mkcert >/dev/null 2>&1; then
24
+ echo "ERROR: mkcert not installed. Run: brew install mkcert nss && mkcert -install" >&2
25
+ exit 1
26
+ fi
27
+
28
+ # First run on this machine? Install the local CA into the system trust store.
29
+ if [ ! -f "$(mkcert -CAROOT)/rootCA.pem" ]; then
30
+ echo "==> Installing mkcert local CA (one-time, may prompt for sudo)..."
31
+ mkcert -install
32
+ fi
33
+
34
+ mkdir -p "${CERTS_DIR}"
35
+
36
+ # Explicit subdomain SANs alongside the wildcard: Apple's SecureTransport (used
37
+ # by macOS-bundled curl) doesn't match `*.domain` wildcards. Chrome is fine, but
38
+ # pipelines and smoke tests that use system curl break without these.
39
+ NAMES=("${DOMAIN}" "*.${DOMAIN}")
40
+ for sub in $(awk '!/^[[:space:]]*(#|$)/ {print $1}' "${KAYRA_ROOT}"/hosts.conf) "$@"; do
41
+ NAMES+=("${sub}.${DOMAIN}")
42
+ done
43
+ # Keep plain localhost working for native (non-compose) dev.
44
+ NAMES+=(localhost "*.localhost" 127.0.0.1 ::1)
45
+
46
+ echo "==> Generating cert for ${DOMAIN} (${#NAMES[@]} names)..."
47
+ mkcert \
48
+ -cert-file "${CERTS_DIR}/cert.pem" \
49
+ -key-file "${CERTS_DIR}/key.pem" \
50
+ "${NAMES[@]}"
51
+
52
+ chmod 644 "${CERTS_DIR}/cert.pem"
53
+ chmod 600 "${CERTS_DIR}/key.pem"
54
+
55
+ echo "Done — ${CERTS_DIR}/cert.pem + key.pem"
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env bash
2
+ # Builds what a target: home server boots from, since nothing can create it
3
+ # for us: the Ubuntu cloud image, grown to vm.disk, and a seed ISO with the
4
+ # same cloud-init as every other server plus its fixed address. Both go into
5
+ # the hypervisor by hand, once: the image as the VM's disk, the ISO as a CD.
6
+ #
7
+ # Usage: FLEET_ROOT=<fleet> bash scripts/local/home-image.sh <server-name>
8
+ # Writes servers/<name>/vm/ (gitignored): <name>.qcow2, seed.iso.
9
+ set -euo pipefail
10
+
11
+ FLEET_ROOT="${FLEET_ROOT:?the directory of the fleet (servers/), set by kayra}"
12
+ KAYRA_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
13
+ name="${1:?Usage: home-image.sh <server-name>}"
14
+ dir="${FLEET_ROOT}/servers/${name}"
15
+ out="${dir}/vm"
16
+
17
+ # Pinned. Only the first boot uses it; Ansible keeps the server current after.
18
+ image_url=https://cloud-images.ubuntu.com/releases/noble/release-20260926/ubuntu-24.04-server-cloudimg-amd64.img
19
+ image_sha256=6a81c37564db9b1ee84e141922625e1d7c5b389b99bb3c572e0243607d5bb4d2
20
+
21
+ command -v qemu-img >/dev/null || { echo "ERROR: qemu-img not installed. Run: brew install qemu" >&2; exit 1; }
22
+
23
+ address="$(yq '.vm.address' "${dir}/vars.yml")"
24
+ gateway="$(yq '.vm.gateway' "${dir}/vars.yml")"
25
+ disk="$(yq '.vm.disk' "${dir}/vars.yml")"
26
+ key="$(sed -n 's/.*ansible_ssh_private_key_file=\([^ ]*\).*/\1/p' "${dir}/inventory.ini" | head -1).pub"
27
+
28
+ mkdir -p "${out}/seed"
29
+ image="${out}/cloudimg-${image_sha256:0:12}.img"
30
+ if [ ! -f "$image" ]; then
31
+ curl -fL --progress-bar -o "${image}.part" "$image_url"
32
+ mv "${image}.part" "$image"
33
+ fi
34
+ echo "${image_sha256} ${image}" | shasum -a 256 -c --quiet
35
+
36
+ rm -f "${out}/${name}.qcow2"
37
+ qemu-img convert -O qcow2 "$image" "${out}/${name}.qcow2"
38
+ qemu-img resize -q "${out}/${name}.qcow2" "$disk"
39
+
40
+ sed -e "s|[$]{hostname}|${name}|" -e "s|[$]{ssh_pubkey}|$(cat "$key")|" \
41
+ "${KAYRA_ROOT}/cloud-init.yml.tpl" > "${out}/seed/user-data"
42
+ printf 'instance-id: %s\nlocal-hostname: %s\n' "$name" "$name" > "${out}/seed/meta-data"
43
+ cat > "${out}/seed/network-config" <<NETWORK
44
+ version: 2
45
+ ethernets:
46
+ lan:
47
+ match: {name: "e*"}
48
+ addresses: [${address}]
49
+ routes: [{to: default, via: ${gateway}}]
50
+ nameservers: {addresses: [${gateway}]}
51
+ NETWORK
52
+
53
+ rm -f "${out}/seed.iso"
54
+ hdiutil makehybrid -quiet -iso -joliet -default-volume-name cidata -o "${out}/seed.iso" "${out}/seed"
55
+ echo "Built ${out}/${name}.qcow2 (${disk}) and ${out}/seed.iso: import the first as the VM's disk, attach the second as a CD."
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env bash
2
+ # Can Ansible get into a home machine (target: home)? If not, say why, in the
3
+ # terms of the failures seen there, before Ansible's UNREACHABLE does not.
4
+ #
5
+ # Usage: FLEET_ROOT=<fleet> bash scripts/local/home-reach.sh <server-name> <kind>
6
+ set -euo pipefail
7
+
8
+ FLEET_ROOT="${FLEET_ROOT:?the directory of the fleet (servers/), set by kayra}"
9
+ name="${1:?Usage: home-reach.sh <server-name> <kind>}"
10
+ kind="${2:?Usage: home-reach.sh <server-name> <kind>}"
11
+ inv="${FLEET_ROOT}/servers/${name}/inventory.ini"
12
+ field() { sed -n "s/.*${1}=\([^ ]*\).*/\1/p" "$inv" | head -1; }
13
+ host="$(field ansible_host)"
14
+ user="$(field ansible_user)"
15
+ key="$(field ansible_ssh_private_key_file)"
16
+ port="$(field ansible_port)"
17
+
18
+ if out="$(ssh -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new \
19
+ -p "${port:-22}" -i "${key/#\~/$HOME}" "${user}@${host}" true 2>&1)"; then
20
+ exit 0
21
+ fi
22
+
23
+ echo "ERROR: cannot SSH into ${name} (${user}@${host}):" >&2
24
+ echo "$out" | grep -v '^\*\*' | sed 's/^/ /' >&2
25
+ case "$out" in
26
+ *"Connection refused"*)
27
+ if [ "$kind" = nas ]; then
28
+ echo " SSH is off: it is on only on demand, for a set time. UGOS: Control Panel → Terminal → SSH on." >&2
29
+ else
30
+ echo " Nothing listens on port ${port:-22}: is the machine up and sshd running?" >&2
31
+ fi ;;
32
+ *"Permission denied"*)
33
+ echo " The key is refused. Is it in the agent? ssh-add ${key}" >&2
34
+ if [ "$kind" = nas ]; then
35
+ echo " If it is: UGOS resets permissions, and sshd ignores a writable ~/.ssh. On the NAS:" >&2
36
+ echo " chmod 755 ~ && chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys" >&2
37
+ fi ;;
38
+ *"timed out"*|*"No route to host"*|*"not known"*)
39
+ echo " ${host} does not answer: is the machine on, and is this Mac on the home network?" >&2 ;;
40
+ esac
41
+ exit 1
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ # Refuse to change a real server from anything but main as merged.
3
+ #
4
+ # Every kayra command deploys the local checkout, whatever it is. A
5
+ # test server may track a branch (that is how platform changes are tested);
6
+ # every other server is only ever changed from a clean main that matches
7
+ # origin/main, so what runs there is exactly what was reviewed and merged.
8
+ # Uncommitted changes in other servers' folders do not count: a test server's
9
+ # branch pointers must not block a prod deploy.
10
+ #
11
+ # Usage: FLEET_ROOT=<fleet> bash scripts/local/require-main.sh <server-name>
12
+ set -euo pipefail
13
+
14
+ FLEET_ROOT="${FLEET_ROOT:?the directory of the fleet (servers/), set by kayra}"
15
+ name="${1:?Usage: require-main.sh <server-name>}"
16
+ target="$(yq '.target' "${FLEET_ROOT}/servers/${name}/server.yml")"
17
+ [ "$target" != "multipass" ] || exit 0
18
+
19
+ fail() {
20
+ echo "ERROR: ${name} is a ${target} server and is only changed from main as merged." >&2
21
+ echo " $*" >&2
22
+ exit 1
23
+ }
24
+
25
+ cd "$FLEET_ROOT"
26
+ branch="$(git branch --show-current)"
27
+ [ "$branch" = "main" ] || fail "The checkout is on '${branch:-a detached HEAD}'. Merge it, then: git switch main && git pull"
28
+ git fetch -q origin main || fail "Could not fetch origin/main to compare against."
29
+ [ "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" ] \
30
+ || fail "Local main differs from origin/main. Pull, and send unpushed commits through a PR."
31
+
32
+ others=()
33
+ for dir in servers/*/; do
34
+ [ "$dir" = "servers/${name}/" ] || others+=(":(exclude)${dir}")
35
+ done
36
+ dirty="$(git status --porcelain --untracked-files=no -- . "${others[@]}")"
37
+ [ -z "$dirty" ] || fail "Uncommitted changes would be deployed; commit them through a PR first:
38
+ ${dirty}"
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env bash
2
+ # Point <domain> and its subdomains at <ip> in /etc/hosts. Requires sudo.
3
+ # Subdomains: the platform's (hosts.conf) plus the given ones —
4
+ # callers pass their apps' subdomains, as they do to certs.sh.
5
+ #
6
+ # Usage: bash scripts/local/setup-hosts.sh <domain> <ip> [subdomain…]
7
+ set -euo pipefail
8
+
9
+ KAYRA_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
10
+ DOMAIN="${1:?Usage: setup-hosts.sh <domain> <ip>}"
11
+ IP="${2:?Usage: setup-hosts.sh <domain> <ip> [subdomain…]}"
12
+ shift 2
13
+
14
+ echo "==> Updating /etc/hosts for ${DOMAIN} -> ${IP}..."
15
+
16
+ if grep -q "${DOMAIN}" /etc/hosts 2>/dev/null; then
17
+ echo " Removing old entries..."
18
+ sudo sed -i.bak "/${DOMAIN}/d" /etc/hosts
19
+ fi
20
+
21
+ HOSTS=""
22
+ for sub in $(awk '!/^[[:space:]]*(#|$)/ {print $1}' "${KAYRA_ROOT}"/hosts.conf) "$@"; do
23
+ HOSTS="${HOSTS} ${sub}.${DOMAIN}"
24
+ done
25
+
26
+ echo "${IP} ${DOMAIN}${HOSTS}" | sudo tee -a /etc/hosts > /dev/null
27
+
28
+ echo ""
29
+ echo "Done — /etc/hosts updated:"
30
+ grep "${DOMAIN}" /etc/hosts
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+ # =============================================================================
3
+ # app-purge <app> — delete a removed app's Docker volumes, i.e. its data.
4
+ #
5
+ # Refuses while the app is still registered here: purging is the second,
6
+ # deliberate step after `app-remove`, never a way around it.
7
+ #
8
+ # Installed by the platform role at <platform_dir>/bin. Run as root.
9
+ # =============================================================================
10
+ set -euo pipefail
11
+
12
+ app="${1:?Usage: app-purge <app>}"
13
+ platform_dir="$(cd "$(dirname "$0")/.." && pwd)"
14
+
15
+ if [ -e "${platform_dir}/jenkins/casc/10-${app}.yaml" ]; then
16
+ echo "ERROR: ${app} is still registered on this server — remove it from server.yml and run \`kayra apps <server>\` first." >&2
17
+ exit 1
18
+ fi
19
+ volumes=$(docker volume ls -q --filter "label=com.docker.compose.project=${app}")
20
+ if [ -z "$volumes" ]; then
21
+ echo "No volumes left for ${app}."
22
+ exit 0
23
+ fi
24
+ # Word-splitting on purpose: one volume name per line, none contain spaces.
25
+ # shellcheck disable=SC2086
26
+ docker volume rm $volumes