@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,14 @@
1
+ #cloud-config
2
+ # Only what Ansible needs to get in. Everything else (packages, Docker, swap,
3
+ # SSH hardening) is the common and docker roles' job: they run on every
4
+ # deploy, while this runs once on first boot and can never be re-applied.
5
+
6
+ hostname: ${hostname}
7
+
8
+ users:
9
+ - name: deploy
10
+ groups: sudo
11
+ shell: /bin/bash
12
+ sudo: ALL=(ALL) NOPASSWD:ALL
13
+ ssh_authorized_keys:
14
+ - ${ssh_pubkey}
@@ -0,0 +1,138 @@
1
+ # =============================================================================
2
+ # Authelia — login for the platform's own pages (Jenkins, Traefik's
3
+ # dashboard). Apps bring their own login; their users never live here.
4
+ #
5
+ # Authelia does NOT expand ${VAR} the way docker compose does. Interpolation here
6
+ # comes from the `template` config filter (X_AUTHELIA_CONFIG_FILTERS=template),
7
+ # which is Go templating: {{ env "DOMAIN" }}.
8
+ #
9
+ # Secrets are never in this file — they arrive as AUTHELIA_*_FILE env vars.
10
+ # =============================================================================
11
+
12
+ theme: 'auto'
13
+
14
+ server:
15
+ address: 'tcp://0.0.0.0:9091/'
16
+
17
+ log:
18
+ level: 'info'
19
+ format: 'text'
20
+
21
+ # Passkeys are the point of this setup; TOTP and Duo would only be extra
22
+ # surface to configure and explain.
23
+ totp:
24
+ disable: true
25
+ duo_api:
26
+ disable: true
27
+
28
+ webauthn:
29
+ disable: false
30
+ enable_passkey_login: true
31
+ # A passkey assertion carrying the user-verification flag satisfies the
32
+ # two_factor policy on its own, so login is one Touch ID prompt rather than
33
+ # password-then-passkey. A stolen bootstrap password alone reaches nothing.
34
+ experimental_enable_passkey_uv_two_factors: true
35
+ display_name: '{{ env "DOMAIN" }}'
36
+ attestation_conveyance_preference: 'indirect'
37
+ timeout: '60 seconds'
38
+ selection_criteria:
39
+ attachment: ''
40
+ discoverability: 'required'
41
+ # 'required', not the default 'preferred': with 'preferred' an authenticator
42
+ # may omit the user-verification flag, and the user is then stuck unable to
43
+ # satisfy a two_factor policy.
44
+ user_verification: 'required'
45
+
46
+ identity_validation:
47
+ elevated_session:
48
+ code_lifespan: '5 minutes'
49
+ elevation_lifespan: '10 minutes'
50
+ characters: 8
51
+ require_second_factor: false
52
+ # Once someone has one passkey, registering a second skips the one-time
53
+ # code entirely.
54
+ skip_second_factor: true
55
+
56
+ authentication_backend:
57
+ refresh_interval: 'always'
58
+ # Both disabled so Authelia never writes back to users_database.yml. That
59
+ # file is rendered from vault on every platform run and would otherwise be
60
+ # silently reverted.
61
+ password_reset:
62
+ disable: true
63
+ password_change:
64
+ disable: true
65
+ file:
66
+ path: '/config/users_database.yml'
67
+ watch: true
68
+ search:
69
+ email: true
70
+ case_insensitive: true
71
+ password:
72
+ algorithm: 'argon2'
73
+ argon2:
74
+ variant: 'argon2id'
75
+ iterations: 3
76
+ # KiB, allocated per concurrent hash. The default 64 MiB is too fat for
77
+ # a 4 GB box with no headroom; 32 MiB is still strong for a credential
78
+ # used once per person, at enrolment.
79
+ memory: 32768
80
+ parallelism: 2
81
+ key_length: 32
82
+ salt_length: 16
83
+
84
+ # AUTH_POLICY is always set by the platform role (default two_factor); test
85
+ # lowers it to one_factor so staging does not need passkey enrolment.
86
+ access_control:
87
+ default_policy: 'deny'
88
+ rules:
89
+ - domain: 'jenkins.{{ env "DOMAIN" }}'
90
+ policy: '{{ env "AUTH_POLICY" }}'
91
+ subject:
92
+ - 'group:admin'
93
+ - domain: 'traefik.{{ env "DOMAIN" }}'
94
+ policy: '{{ env "AUTH_POLICY" }}'
95
+ subject:
96
+ - 'group:admin'
97
+
98
+ session:
99
+ name: 'authelia_session'
100
+ same_site: 'lax'
101
+ # A working day of idle time. Shorter is tempting, but a session that dies
102
+ # mid-form turns the next POST into a 302 to the portal and the body is lost.
103
+ inactivity: '8 hours'
104
+ expiration: '1 day'
105
+ remember_me: '1 week'
106
+ cookies:
107
+ # The apex, not auth.<DOMAIN> — the cookie has to reach jenkins.<DOMAIN>
108
+ # and traefik.<DOMAIN> too. Authelia requires this to be a suffix of (or equal to) authelia_url.
109
+ - domain: '{{ env "DOMAIN" }}'
110
+ authelia_url: 'https://auth.{{ env "DOMAIN" }}'
111
+
112
+ regulation:
113
+ max_retries: 3
114
+ find_time: '2 minutes'
115
+ ban_time: '5 minutes'
116
+
117
+ storage:
118
+ local:
119
+ path: '/data/db.sqlite3'
120
+
121
+ # SMTP when it is configured, otherwise write to a file. The notifier is only
122
+ # used for the one-time code at passkey enrolment, so the file fallback keeps
123
+ # dev working without an external dependency.
124
+ notifier:
125
+ disable_startup_check: false
126
+ {{- if env "SMTP_ADDRESS" }}
127
+ smtp:
128
+ address: '{{ env "SMTP_ADDRESS" }}'
129
+ username: '{{ env "SMTP_USERNAME" }}'
130
+ sender: '{{ env "SMTP_SENDER" }}'
131
+ # In the template rather than an env var on purpose: an empty
132
+ # AUTHELIA_NOTIFIER_SMTP_PASSWORD still creates notifier.smtp, and Authelia
133
+ # then refuses to start with both notifiers configured.
134
+ password: {{ secret "/config/secrets/smtp_password" | msquote }}
135
+ {{- else }}
136
+ filesystem:
137
+ filename: '/data/notification.txt'
138
+ {{- end }}
@@ -0,0 +1,14 @@
1
+ # BuildKit daemon config.
2
+ #
3
+ # The registry is plain HTTP on the internal docker network and has no TLS
4
+ # anywhere. BuildKit has to be told that explicitly or it tries HTTPS.
5
+
6
+ [worker.oci]
7
+ enabled = true
8
+ # Rootless: no process sandbox, and the rootless worker cannot use overlayfs
9
+ # on every kernel, so let BuildKit pick a snapshotter it can actually run.
10
+ max-parallelism = 2
11
+
12
+ [registry."registry:5000"]
13
+ http = true
14
+ insecure = true
@@ -0,0 +1,12 @@
1
+ # =============================================================================
2
+ # Overlay for a server whose certificates come by DNS-01 (acme_dns in its
3
+ # vars.yml): lego, inside Traefik, reads the zone's token from a file. Added to
4
+ # COMPOSE_FILE only there, so other servers' Traefik stays as it is.
5
+ # =============================================================================
6
+
7
+ services:
8
+ traefik:
9
+ environment:
10
+ HETZNER_API_TOKEN_FILE: /run/secrets/acme-dns-token
11
+ volumes:
12
+ - ./traefik/secrets:/run/secrets:ro
@@ -0,0 +1,156 @@
1
+ # =============================================================================
2
+ # Platform CI overlay — Docker Registry + Jenkins (controller + agent)
3
+ # App-agnostic: provides CI/CD infrastructure for any application.
4
+ # Usage: docker compose -f docker-compose.yml -f docker-compose.ci.yml up
5
+ # =============================================================================
6
+
7
+ services:
8
+ registry:
9
+ image: registry:2.8.3
10
+ restart: unless-stopped
11
+ environment:
12
+ # registry-gc.sh deletes manifests nothing references any more.
13
+ - REGISTRY_STORAGE_DELETE_ENABLED=true
14
+ # No Traefik router: builds push to registry:5000 and the host daemon pulls
15
+ # from localhost:5000, so nothing outside the server needs it.
16
+ ports:
17
+ - "127.0.0.1:5000:5000"
18
+ volumes:
19
+ - registry-data:/var/lib/registry
20
+ networks:
21
+ - internal
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # BuildKit — builds images without anyone holding the host's docker socket.
25
+ #
26
+ # The build agent used to mount /var/run/docker.sock read-write while running
27
+ # pipeline code checked out from arbitrary branches, which is host root by
28
+ # design rather than by exploit. Builds now go here instead: rootless, its own
29
+ # daemon, pushing straight to the registry. Compromising it gets you a
30
+ # container, not the box.
31
+ # ---------------------------------------------------------------------------
32
+ buildkitd:
33
+ image: moby/buildkit:v0.34.0-rootless
34
+ restart: unless-stopped
35
+ # Flags go in the command: this image does not read BUILDKITD_FLAGS (that
36
+ # is a buildx convention), and without them it listens on a unix socket
37
+ # the agent cannot reach.
38
+ command:
39
+ - --oci-worker-no-process-sandbox
40
+ - --addr
41
+ - tcp://0.0.0.0:1234
42
+ # rootlesskit needs an unprivileged user namespace, which Ubuntu 23.10+
43
+ # denies to unconfined processes. The named profile (installed by the docker
44
+ # role) grants userns to this container alone, so the host keeps the
45
+ # restriction.
46
+ security_opt:
47
+ - seccomp=unconfined
48
+ - apparmor=buildkit-rootless
49
+ volumes:
50
+ - buildkit-cache:/home/user/.local/share/buildkit
51
+ - ./buildkit/buildkitd.toml:/home/user/.config/buildkit/buildkitd.toml:ro
52
+ mem_limit: 1g
53
+ networks:
54
+ - internal
55
+
56
+ jenkins-controller:
57
+ build:
58
+ context: ./jenkins
59
+ dockerfile: Dockerfile
60
+ # The controller runs as jenkins (uid/gid 1000) and reaches the socket via
61
+ # the host's docker group, whose GID differs per host.
62
+ group_add:
63
+ - ${DOCKER_GID:?}
64
+ restart: unless-stopped
65
+ # No COMPOSE_PROJECT_NAME here on purpose: every app's deploy runs
66
+ # `docker compose` from this container, and an exported project name would
67
+ # beat the one the deploy passes. APPS_DIR is where app .env files live.
68
+ environment:
69
+ - JENKINS_ADMIN_USER=${JENKINS_ADMIN_USER:-admin}
70
+ - JENKINS_ADMIN_PASSWORD=${JENKINS_ADMIN_PASSWORD:-admin}
71
+ - JENKINS_GIT_REPO=${GIT_REMOTE_URL:?}
72
+ - GITHUB_WEBHOOK_SECRET=${GITHUB_WEBHOOK_SECRET:-}
73
+ - DOMAIN=${DOMAIN:-localhost}
74
+ - APPS_DIR=${APPS_DIR:?}
75
+ - PLATFORM_REF=${PLATFORM_REF:?}
76
+ - PLATFORM_RENOVATE=${PLATFORM_RENOVATE:-false}
77
+ - RENOVATE_SCHEDULE=${RENOVATE_SCHEDULE:-H 4 * * 1}
78
+ - RENOVATE_PLATFORM=${RENOVATE_PLATFORM:-github}
79
+ - RENOVATE_ENDPOINT=${RENOVATE_ENDPOINT:-}
80
+ - RENOVATE_GIT_URL=${RENOVATE_GIT_URL:-}
81
+ - RENOVATE_GITHUB_COM_TOKEN=${RENOVATE_GITHUB_COM_TOKEN:-}
82
+ - RENOVATE_HOST_RULES=${RENOVATE_HOST_RULES:-}
83
+ - RENOVATE_REPOSITORIES=${RENOVATE_REPOSITORIES:-}
84
+ # Tell JCasC to merge every YAML in this directory at startup.
85
+ - CASC_JENKINS_CONFIG=/var/jenkins_home/casc
86
+ volumes:
87
+ - jenkins-home:/var/jenkins_home
88
+ # The controller is the only node with the docker socket, and it runs
89
+ # the deploy stage, so it needs the app's compose directory too.
90
+ - /var/run/docker.sock:/var/run/docker.sock
91
+ - ${APPS_DIR:?}:${APPS_DIR:?}:ro
92
+ - ./jenkins/casc:/var/jenkins_home/casc:ro
93
+ # One directory per app, one file per secret; JCasC reads them with
94
+ # ${readFile:...}. Written by bin/jenkins-app-apply.
95
+ - ./jenkins-secrets:/run/app-secrets:ro
96
+ # GIT_TOKEN and RENOVATE_TOKEN, likewise: files, so a new one needs a
97
+ # reload, not a restart. Written by the platform role (or forgejo-config).
98
+ - ./platform-secrets:/run/platform-secrets:ro
99
+ labels:
100
+ - "traefik.enable=true"
101
+ - "traefik.docker.network=platform-internal"
102
+ - "traefik.http.routers.jenkins.rule=Host(`jenkins.${DOMAIN:-localhost}`)"
103
+ - "traefik.http.routers.jenkins.entrypoints=websecure"
104
+ - "traefik.http.routers.jenkins.tls.certresolver=le"
105
+ - "traefik.http.routers.jenkins.middlewares=authelia@file"
106
+ - "traefik.http.routers.jenkins.service=jenkins"
107
+ # GitHub cannot log in; the plugin checks the payload's HMAC instead.
108
+ - "traefik.http.routers.jenkins-webhook.rule=Host(`jenkins.${DOMAIN:-localhost}`) && Path(`/github-webhook/`)"
109
+ - "traefik.http.routers.jenkins-webhook.entrypoints=websecure"
110
+ - "traefik.http.routers.jenkins-webhook.tls.certresolver=le"
111
+ - "traefik.http.routers.jenkins-webhook.priority=100"
112
+ - "traefik.http.routers.jenkins-webhook.service=jenkins"
113
+ - "traefik.http.services.jenkins.loadbalancer.server.port=8080"
114
+ networks:
115
+ - internal
116
+ healthcheck:
117
+ test: ["CMD", "curl", "-sf", "http://127.0.0.1:8080/login"]
118
+ interval: 15s
119
+ timeout: 5s
120
+ retries: 10
121
+ start_period: 60s
122
+
123
+ jenkins-agent:
124
+ build:
125
+ context: ./jenkins-agent
126
+ dockerfile: Dockerfile
127
+ restart: unless-stopped
128
+ # Written by the platform role once Jenkins is up; absent on first start,
129
+ # when the agent crash-loops until the role recreates it with the secret.
130
+ env_file:
131
+ - path: ./jenkins-agent.env
132
+ required: false
133
+ environment:
134
+ - JENKINS_URL=http://jenkins-controller:8080
135
+ - JENKINS_AGENT_NAME=builder
136
+ - JENKINS_AGENT_WORKDIR=/home/jenkins/agent
137
+ - DOMAIN=${DOMAIN:-localhost}
138
+ # Builds go to buildkitd, not to a docker daemon. This agent runs pipeline
139
+ # code from untrusted branches and deliberately has no docker socket.
140
+ - BUILDKIT_HOST=tcp://buildkitd:1234
141
+ - REGISTRY_PUSH_HOST=registry:5000
142
+ volumes:
143
+ - jenkins-agent-workdir:/home/jenkins/agent
144
+ networks:
145
+ - internal
146
+ depends_on:
147
+ jenkins-controller:
148
+ condition: service_healthy
149
+ buildkitd:
150
+ condition: service_started
151
+
152
+ volumes:
153
+ registry-data:
154
+ buildkit-cache:
155
+ jenkins-home:
156
+ jenkins-agent-workdir:
@@ -0,0 +1,83 @@
1
+ # =============================================================================
2
+ # Platform base — Traefik reverse proxy + shared network
3
+ # App-agnostic: knows nothing about what applications run on this platform.
4
+ # =============================================================================
5
+
6
+ services:
7
+ traefik:
8
+ image: traefik:v3.7.14
9
+ restart: unless-stopped
10
+ ports:
11
+ - "80:80"
12
+ - "443:443"
13
+ volumes:
14
+ - /var/run/docker.sock:/var/run/docker.sock:ro
15
+ - ./traefik/traefik.yml:/etc/traefik/traefik.yml:ro
16
+ - ./traefik/dynamic:/etc/traefik/dynamic:ro
17
+ - ./traefik/certs:/certs:ro
18
+ - acme-data:/acme
19
+ networks:
20
+ - public
21
+ - internal
22
+ labels:
23
+ # The dashboard used to ride on an unauthenticated published :8080.
24
+ # It speaks no auth of its own, which is exactly what forwardAuth is for.
25
+ - "traefik.enable=true"
26
+ - "traefik.http.routers.dashboard.rule=Host(`traefik.${DOMAIN:-localhost}`)"
27
+ - "traefik.http.routers.dashboard.entrypoints=websecure"
28
+ - "traefik.http.routers.dashboard.tls.certresolver=le"
29
+ - "traefik.http.routers.dashboard.service=api@internal"
30
+ - "traefik.http.routers.dashboard.middlewares=authelia@file,security-headers@file"
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Authelia — single sign-on for services that sit behind Traefik.
34
+ #
35
+ # Deliberately NOT behind its own authelia@file middleware: that would be an
36
+ # infinite redirect. The portal has to stay reachable to authenticate anyone.
37
+ # ---------------------------------------------------------------------------
38
+ authelia:
39
+ image: authelia/authelia:4.39.28
40
+ restart: unless-stopped
41
+ environment:
42
+ - X_AUTHELIA_CONFIG=/config/configuration.yml
43
+ # Authelia does not expand ${VAR}; this filter is what makes
44
+ # {{ env "DOMAIN" }} work inside configuration.yml.
45
+ - X_AUTHELIA_CONFIG_FILTERS=template
46
+ - DOMAIN=${DOMAIN:-localhost}
47
+ - AUTH_POLICY=${AUTH_POLICY:-two_factor}
48
+ - AUTHELIA_SESSION_SECRET_FILE=/config/secrets/session
49
+ - AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE=/config/secrets/storage_encryption
50
+ # Empty in dev, which falls the notifier back to a file — see
51
+ # configuration.yml. The SMTP password is NOT an env var; it is read from
52
+ # /config/secrets/smtp_password by the config template.
53
+ - SMTP_ADDRESS=${AUTHELIA_SMTP_ADDRESS:-}
54
+ - SMTP_USERNAME=${AUTHELIA_SMTP_USERNAME:-}
55
+ - SMTP_SENDER=${AUTHELIA_SMTP_SENDER:-}
56
+ volumes:
57
+ - ./authelia:/config:ro
58
+ - authelia-data:/data
59
+ mem_limit: 128m
60
+ networks:
61
+ - internal
62
+ labels:
63
+ - "traefik.enable=true"
64
+ - "traefik.docker.network=platform-internal"
65
+ - "traefik.http.routers.authelia.rule=Host(`auth.${DOMAIN:-localhost}`)"
66
+ - "traefik.http.routers.authelia.entrypoints=websecure"
67
+ - "traefik.http.routers.authelia.tls.certresolver=le"
68
+ - "traefik.http.services.authelia.loadbalancer.server.port=9091"
69
+
70
+ # Two networks, so an app can reach Traefik's routes and nothing else of the
71
+ # platform. Apps join platform-public; the platform's own services live on
72
+ # platform-internal, where only Traefik joins them.
73
+ networks:
74
+ public:
75
+ name: platform-public
76
+ internal:
77
+ name: platform-internal
78
+
79
+ volumes:
80
+ acme-data:
81
+ # Passkeys live here. Losing this volume means every credential is gone and
82
+ # everyone re-enrols — back it up.
83
+ authelia-data:
@@ -0,0 +1,40 @@
1
+ # =============================================================================
2
+ # Jenkins Controller — LTS with Docker CLI, Compose, buildctl and JCasC
3
+ # =============================================================================
4
+
5
+ FROM jenkins/jenkins:2.580.1-lts-jdk21
6
+
7
+ USER root
8
+
9
+ # renovate: datasource=github-releases depName=moby/buildkit
10
+ ARG BUILDKIT_VERSION=v0.34.0
11
+
12
+ # Install Docker CLI + Compose plugin
13
+ RUN apt-get update && \
14
+ apt-get install -y --no-install-recommends \
15
+ ca-certificates curl gnupg jq && \
16
+ install -m 0755 -d /etc/apt/keyrings && \
17
+ curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg && \
18
+ chmod a+r /etc/apt/keyrings/docker.gpg && \
19
+ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
20
+ https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
21
+ > /etc/apt/sources.list.d/docker.list && \
22
+ apt-get update && \
23
+ apt-get install -y --no-install-recommends \
24
+ docker-ce-cli docker-compose-plugin && \
25
+ arch="$(dpkg --print-architecture)" && \
26
+ curl -fsSL "https://github.com/moby/buildkit/releases/download/${BUILDKIT_VERSION}/buildkit-${BUILDKIT_VERSION}.linux-${arch}.tar.gz" \
27
+ | tar -xz -C /usr/local bin/buildctl && \
28
+ rm -rf /var/lib/apt/lists/*
29
+
30
+ USER jenkins
31
+
32
+ # Install plugins
33
+ COPY plugins.txt /usr/share/jenkins/ref/plugins.txt
34
+ RUN jenkins-plugin-cli --plugin-file /usr/share/jenkins/ref/plugins.txt
35
+
36
+ # JCasC fragments are bind-mounted, not baked in; CASC_JENKINS_CONFIG is set in
37
+ # compose. That keeps config out of the image and out of the jenkins-home volume.
38
+ # notifyCommit without a token, but only to trigger polling: how Forgejo tells
39
+ # Jenkins about a push, over platform-internal (Traefik does not route it).
40
+ ENV JAVA_OPTS="-Djenkins.install.runSetupWizard=false -Dhudson.plugins.git.GitStatus.NOTIFY_COMMIT_ACCESS_CONTROL=disabled-for-polling"
@@ -0,0 +1,158 @@
1
+ # =============================================================================
2
+ # Jenkins Configuration as Code — platform fragment (app-agnostic).
3
+ #
4
+ # Owns:
5
+ # - Jenkins setup, security realm, authorization
6
+ # - The agent node
7
+ # - Git credentials (for cloning the app repo)
8
+ # - GitHub: push-webhook secret + API token for commit statuses
9
+ # - Jenkins URL
10
+ #
11
+ # Also owns the app-agnostic jobs: platform-registry-gc and rollback work off
12
+ # the release records every app's <app>/deploy job writes (the platform
13
+ # library's appDeploy) and start that job with a REF. platform-renovate exists
14
+ # only where the server has repos to keep up to date (PLATFORM_RENOVATE).
15
+ #
16
+ # Does NOT own any app-specific credentials (Postgres, ...) or app pipelines.
17
+ # Each app renders its own 10-<app>.yaml next to this file and applies it with
18
+ # bin/jenkins-app-apply. JCasC merges every YAML in this directory.
19
+ #
20
+ # Git remote comes from the platform .env (rendered by Ansible).
21
+ # =============================================================================
22
+
23
+ jenkins:
24
+ systemMessage: "Platform CI/CD — Managed by JCasC"
25
+ numExecutors: 1
26
+
27
+ securityRealm:
28
+ local:
29
+ allowsSignup: false
30
+ users:
31
+ - id: "${JENKINS_ADMIN_USER:-admin}"
32
+ password: "${JENKINS_ADMIN_PASSWORD:-admin}"
33
+
34
+ authorizationStrategy:
35
+ loggedInUsersCanDoAnything:
36
+ allowAnonymousRead: false
37
+
38
+ nodes:
39
+ - permanent:
40
+ name: "builder"
41
+ labelString: "buildkit linux"
42
+ numExecutors: 2
43
+ remoteFS: "/home/jenkins/agent"
44
+ launcher:
45
+ inbound:
46
+ workDirSettings:
47
+ disabled: false
48
+ internalDir: "remoting"
49
+ retentionStrategy: "always"
50
+
51
+ credentials:
52
+ system:
53
+ domainCredentials:
54
+ - credentials:
55
+ - usernamePassword:
56
+ scope: GLOBAL
57
+ id: "git-credentials"
58
+ description: "Git credentials for cloning repositories"
59
+ username: "git"
60
+ password: "${readFile:/run/platform-secrets/GIT_TOKEN}"
61
+ - string:
62
+ scope: GLOBAL
63
+ id: "github-token"
64
+ description: "Same PAT as git-credentials; needs Commit statuses: write"
65
+ secret: "${readFile:/run/platform-secrets/GIT_TOKEN}"
66
+ - string:
67
+ scope: GLOBAL
68
+ id: "renovate-token"
69
+ description: "PAT for Renovate: Contents + Pull requests read & write"
70
+ secret: "${readFile:/run/platform-secrets/RENOVATE_TOKEN}"
71
+ - string:
72
+ scope: GLOBAL
73
+ id: "github-webhook-secret"
74
+ description: "HMAC secret GitHub signs push webhooks with"
75
+ secret: "${GITHUB_WEBHOOK_SECRET:-}"
76
+
77
+ unclassified:
78
+ # The pipeline every app runs. Trusted (outside the sandbox), loaded from
79
+ # PLATFORM_REF (the release tag, v<version>; a test server may set a branch) and jobs cannot
80
+ # pick another version, so a pushed app branch never changes what a pipeline
81
+ # does. See ci/jenkins-lib/.
82
+ globalLibraries:
83
+ libraries:
84
+ - name: "platform"
85
+ defaultVersion: "${PLATFORM_REF}"
86
+ allowVersionOverride: false
87
+ implicit: false
88
+ retriever:
89
+ modernSCM:
90
+ libraryPath: "ci/jenkins-lib/"
91
+ scm:
92
+ git:
93
+ remote: "${JENKINS_GIT_REPO}"
94
+ credentialsId: "git-credentials"
95
+ location:
96
+ url: "https://jenkins.${DOMAIN:-localhost}/"
97
+ # Webhook lands on /github-webhook/. Hooks are registered by hand in the
98
+ # repo settings, not managed from here.
99
+ gitHubPluginConfig:
100
+ configs:
101
+ - name: "GitHub"
102
+ apiUrl: "https://api.github.com"
103
+ credentialsId: "github-token"
104
+ manageHooks: false
105
+ hookSecretConfigs:
106
+ - credentialsId: "github-webhook-secret"
107
+
108
+ # Platform jobs run pipelines from PLATFORM_REF of this repo, never from a pushed branch.
109
+ jobs:
110
+ - script: |
111
+ def fromPlatform = { String path ->
112
+ return {
113
+ cpsScm {
114
+ scm {
115
+ git {
116
+ remote {
117
+ url("${JENKINS_GIT_REPO}")
118
+ credentials("git-credentials")
119
+ }
120
+ branch("${PLATFORM_REF}")
121
+ }
122
+ }
123
+ scriptPath(path)
124
+ lightweight(false)
125
+ }
126
+ }
127
+ }
128
+
129
+ pipelineJob("platform-registry-gc") {
130
+ displayName("Platform Registry GC")
131
+ description("Nightly: deletes registry images no recent release of any app references, then prunes the BuildKit cache and host images. See ci/registry-gc.sh.")
132
+ definition(fromPlatform("ci/registry-gc.Jenkinsfile"))
133
+ triggers {
134
+ cron("H 3 * * *")
135
+ }
136
+ }
137
+
138
+ pipelineJob("rollback") {
139
+ displayName("Rollback")
140
+ description("Redeploys an earlier release of an app: pick the app, pick a release, done. Hands the SHA to <app>/deploy.")
141
+ definition(fromPlatform("ci/rollback.Jenkinsfile"))
142
+ }
143
+
144
+ if ("${PLATFORM_RENOVATE:-false}" == "true") {
145
+ pipelineJob("platform-renovate") {
146
+ displayName("Platform Renovate")
147
+ description("On renovate_schedule (weekly by default): opens PRs that bump pinned versions. Config: renovate.json. See ci/renovate.Jenkinsfile.")
148
+ // Declared here as well: a JCasC reload regenerates the job and drops
149
+ // parameters only the pipeline declares.
150
+ parameters {
151
+ booleanParam("DRY_RUN", false, "Log what would change, open no PRs")
152
+ }
153
+ definition(fromPlatform("ci/renovate.Jenkinsfile"))
154
+ triggers {
155
+ cron("${RENOVATE_SCHEDULE:-H 4 * * 1}")
156
+ }
157
+ }
158
+ }