diff --git a/oci/VALIDATION.md b/oci/VALIDATION.md index 9e682809..44a88c6c 100644 --- a/oci/VALIDATION.md +++ b/oci/VALIDATION.md @@ -2,7 +2,7 @@ -44 of 530 applications in the OCI catalog have been tested for real: 41 in the ProxMenux lab and 3 by the community. The OCI installer shows them as verified, with a ✓ in the lists. +44 of 531 applications in the OCI catalog have been tested for real: 41 in the ProxMenux lab and 3 by the community. The OCI installer shows them as verified, with a ✓ in the lists. Each test describes the scenario that was run and names the image it ran on. It does not cover every possible configuration of the application, and a newer image has not been tested until someone reports it. diff --git a/oci/catalog/apps/authentik.json b/oci/catalog/apps/authentik.json new file mode 100644 index 00000000..d466d36f --- /dev/null +++ b/oci/catalog/apps/authentik.json @@ -0,0 +1,645 @@ +{ + "schema_version": "0.5.0", + "kind": "proxmenux.oci-template", + "id": "image-authentik", + "status": "generated-unvalidated", + "catalog_ui": { + "title": { + "en_US": "Authentik" + }, + "tagline": { + "en_US": "Identity provider for single sign-on and access control" + }, + "description": { + "en_US": "Authentik is an open-source identity provider and access platform. This recipe follows the official three-service deployment with PostgreSQL, the server and a worker, each in its own Proxmox OCI LXC." + }, + "category": "security", + "category_label": "Authentication & Security", + "author": "Authentik", + "developer": "Authentik", + "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/authentik.webp", + "thumbnail": null, + "screenshots": [], + "architectures": [ + "amd64", + "arm64" + ], + "launch": { + "scheme": "http", + "port": 9000, + "path": "/" + }, + "website": "https://goauthentik.io/", + "documentation": "https://docs.goauthentik.io/install-config/install/docker-compose/", + "repository": "https://github.com/goauthentik/authentik", + "tips": [ + "The server and worker share /data and /templates through host directories; those directories are not included in Proxmox CT backups.", + "The worker Docker-socket mount is omitted. Use the embedded outpost or deploy and manage a separate outpost manually." + ], + "mini_changelog": [], + "display_version": "2026.8.5", + "updated_at": "2026-10-21" + }, + "source": { + "provider": "goauthentik", + "repository": "https://github.com/goauthentik/authentik", + "default_branch": "main", + "revision": "4ec0d19b98da3c329781894cd550da95d3033954", + "readme_raw_url": "https://goauthentik.io/version/2026.8/lifecycle/container/compose.yml", + "image_repository_url": "https://github.com/goauthentik/authentik/pkgs/container/server", + "readme_pushed_at": "2026-10-21T12:57:59Z", + "compose_sha256": "a3e138b5a85844d15c4a7ba6339997e43c0479d9d198582c67dd19523aadc694", + "generated_at": "2026-10-30T18:09:01+00:00" + }, + "container_contract": { + "service_name": "server", + "container_name": "authentik-server", + "image": { + "reference": "ghcr.io/goauthentik/server:2026.8.5", + "registry": "ghcr.io", + "repository": "ghcr.io/goauthentik/server", + "tag": "2026.8.5", + "digest": null, + "pull_policy": "resolve-written-reference-to-architecture-digest-at-install" + }, + "environment": [ + { + "name": "AUTHENTIK_POSTGRESQL__HOST", + "example": "postgresql", + "required": true, + "sensitive": false, + "source": "docker-compose" + }, + { + "name": "AUTHENTIK_POSTGRESQL__NAME", + "example": "authentik", + "required": true, + "sensitive": false, + "source": "docker-compose" + }, + { + "name": "AUTHENTIK_POSTGRESQL__PASSWORD", + "example": "${GENERATED_GENERATED_PG_PASS}", + "required": true, + "sensitive": true, + "source": "docker-compose" + }, + { + "name": "AUTHENTIK_POSTGRESQL__USER", + "example": "authentik", + "required": true, + "sensitive": false, + "source": "docker-compose" + }, + { + "name": "AUTHENTIK_SECRET_KEY", + "example": "${GENERATED_GENERATED_AUTHENTIK_SECRET_KEY}", + "required": true, + "sensitive": true, + "source": "docker-compose" + } + ], + "volumes": [ + { + "id": "volume-0", + "container_path": "/data", + "compose_source_example": "./data", + "read_only": false, + "required": true, + "installation_choice": [ + "host-bind" + ], + "default": "host-bind", + "managed_volume": { + "backup": false, + "default_size_gb": 8 + } + }, + { + "id": "volume-1", + "container_path": "/templates", + "compose_source_example": "./custom-templates", + "read_only": false, + "required": true, + "installation_choice": [ + "host-bind" + ], + "default": "host-bind", + "managed_volume": { + "backup": false, + "default_size_gb": 8 + } + } + ], + "ports": [ + { + "container_port": 9000, + "published_example": 9000, + "protocol": "tcp", + "required": true, + "proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat" + }, + { + "container_port": 9443, + "published_example": 9443, + "protocol": "tcp", + "required": true, + "proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat" + } + ], + "related_services": [ + { + "name": "postgresql", + "image": "docker.io/library/postgres:16-alpine" + }, + { + "name": "worker", + "image": "ghcr.io/goauthentik/server:2026.8.5" + } + ], + "restart": "unless-stopped", + "stop_grace_period": null, + "original_compose": "name: authentik\nservices:\n postgresql:\n environment:\n POSTGRES_DB: authentik\n POSTGRES_PASSWORD: ${GENERATED_PG_PASS}\n POSTGRES_USER: authentik\n healthcheck:\n interval: 30s\n retries: 5\n start_period: 20s\n test:\n - CMD-SHELL\n - pg_isready -h 127.0.0.1 -d authentik -U authentik\n timeout: 5s\n image: docker.io/library/postgres:16-alpine\n restart: unless-stopped\n volumes:\n - read_only: false\n source: database\n target: /var/lib/postgresql/data\n server:\n command: server\n depends_on:\n postgresql:\n condition: service_healthy\n environment:\n AUTHENTIK_POSTGRESQL__HOST: postgresql\n AUTHENTIK_POSTGRESQL__NAME: authentik\n AUTHENTIK_POSTGRESQL__PASSWORD: ${GENERATED_PG_PASS}\n AUTHENTIK_POSTGRESQL__USER: authentik\n AUTHENTIK_SECRET_KEY: ${GENERATED_AUTHENTIK_SECRET_KEY}\n image: ghcr.io/goauthentik/server:2026.8.5\n ports:\n - 9000:9000\n - 9443:9443\n restart: unless-stopped\n shm_size: 512mb\n volumes:\n - read_only: false\n source: ./data\n target: /data\n - read_only: false\n source: ./custom-templates\n target: /templates\n worker:\n command: worker\n depends_on:\n postgresql:\n condition: service_healthy\n environment:\n AUTHENTIK_POSTGRESQL__HOST: postgresql\n AUTHENTIK_POSTGRESQL__NAME: authentik\n AUTHENTIK_POSTGRESQL__PASSWORD: ${GENERATED_PG_PASS}\n AUTHENTIK_POSTGRESQL__USER: authentik\n AUTHENTIK_SECRET_KEY: ${GENERATED_AUTHENTIK_SECRET_KEY}\n image: ghcr.io/goauthentik/server:2026.8.5\n restart: unless-stopped\n shm_size: 512mb\n user: root\n volumes:\n - read_only: false\n source: ./data\n target: /data\n - read_only: false\n source: ./certs\n target: /certs\n - read_only: false\n source: ./custom-templates\n target: /templates\nvolumes:\n database: {}\n" + }, + "compose_stack": { + "project_name": "authentik", + "deployment_model": "one-native-oci-lxc-per-compose-service", + "user_experience": "single-application-install", + "main_service": "server", + "service_count": 3, + "services": [ + { + "name": "postgresql", + "image": "docker.io/library/postgres:16-alpine", + "is_main": false, + "role": "dependency", + "vmid_offset": 1, + "depends_on": [], + "frontend_network": false, + "private_network": true, + "compose": { + "environment": { + "POSTGRES_DB": "authentik", + "POSTGRES_PASSWORD": "${GENERATED_PG_PASS}", + "POSTGRES_USER": "authentik" + }, + "healthcheck": { + "interval": "30s", + "retries": 5, + "start_period": "20s", + "test": [ + "CMD-SHELL", + "pg_isready -h 127.0.0.1 -d authentik -U authentik" + ], + "timeout": "5s" + }, + "image": "docker.io/library/postgres:16-alpine", + "restart": "unless-stopped", + "volumes": [ + { + "source": "database", + "target": "/var/lib/postgresql/data", + "read_only": false + } + ] + } + }, + { + "name": "worker", + "image": "ghcr.io/goauthentik/server:2026.8.5", + "is_main": false, + "role": "dependency", + "vmid_offset": 2, + "depends_on": [ + "postgresql" + ], + "frontend_network": false, + "private_network": true, + "compose": { + "command": "worker", + "depends_on": { + "postgresql": { + "condition": "service_healthy" + } + }, + "environment": { + "AUTHENTIK_POSTGRESQL__HOST": "postgresql", + "AUTHENTIK_POSTGRESQL__NAME": "authentik", + "AUTHENTIK_POSTGRESQL__PASSWORD": "${GENERATED_PG_PASS}", + "AUTHENTIK_POSTGRESQL__USER": "authentik", + "AUTHENTIK_SECRET_KEY": "${GENERATED_AUTHENTIK_SECRET_KEY}" + }, + "image": "ghcr.io/goauthentik/server:2026.8.5", + "restart": "unless-stopped", + "shm_size": "512mb", + "user": "root", + "volumes": [ + { + "source": "./data", + "target": "/data", + "read_only": false + }, + { + "source": "./certs", + "target": "/certs", + "read_only": false + }, + { + "source": "./custom-templates", + "target": "/templates", + "read_only": false + } + ] + } + }, + { + "name": "server", + "image": "ghcr.io/goauthentik/server:2026.8.5", + "is_main": true, + "role": "frontend", + "vmid_offset": 0, + "depends_on": [ + "postgresql" + ], + "frontend_network": true, + "private_network": true, + "compose": { + "command": "server", + "depends_on": { + "postgresql": { + "condition": "service_healthy" + } + }, + "environment": { + "AUTHENTIK_POSTGRESQL__HOST": "postgresql", + "AUTHENTIK_POSTGRESQL__NAME": "authentik", + "AUTHENTIK_POSTGRESQL__PASSWORD": "${GENERATED_PG_PASS}", + "AUTHENTIK_POSTGRESQL__USER": "authentik", + "AUTHENTIK_SECRET_KEY": "${GENERATED_AUTHENTIK_SECRET_KEY}" + }, + "image": "ghcr.io/goauthentik/server:2026.8.5", + "ports": [ + "9000:9000", + "9443:9443" + ], + "restart": "unless-stopped", + "shm_size": "512mb", + "volumes": [ + { + "source": "./data", + "target": "/data", + "read_only": false + }, + { + "source": "./custom-templates", + "target": "/templates", + "read_only": false + } + ] + } + } + ], + "top_level": { + "name": "authentik", + "volumes": { + "database": {} + } + }, + "networking": { + "frontend": "selected-proxmox-bridge", + "private_required": true, + "private_creation": "automatic-create-if-missing", + "private_address_allocation": "automatic-static-address-per-service", + "service_discovery": "private-addresses-with-compose-service-host-aliases", + "dependency_external_access": "disabled-unless-service-publishes-ports", + "prompt_user_for_private_network": false + }, + "storage": [ + { + "id": "postgresql-volume-0", + "service": "postgresql", + "container_path": "/var/lib/postgresql/data", + "mode": "managed-volume", + "user_selectable": false, + "backup": true, + "shared_with_other_lxc": false, + "source_path": null, + "source_path_prompt": null + }, + { + "id": "server-volume-0", + "service": "server", + "container_path": "/data", + "mode": "host-bind", + "user_selectable": true, + "backup": false, + "shared_with_other_lxc": true, + "source_path": null, + "source_path_prompt": "Host directory for authentik:/data" + }, + { + "id": "server-volume-1", + "service": "server", + "container_path": "/templates", + "mode": "host-bind", + "user_selectable": true, + "backup": false, + "shared_with_other_lxc": true, + "source_path": null, + "source_path_prompt": "Host directory for authentik:/templates" + }, + { + "id": "worker-volume-0", + "service": "worker", + "container_path": "/data", + "mode": "host-bind", + "user_selectable": true, + "backup": false, + "shared_with_other_lxc": true, + "source_path": null, + "source_path_prompt": "Host directory for authentik:/data" + }, + { + "id": "worker-volume-1", + "service": "worker", + "container_path": "/certs", + "mode": "managed-volume", + "user_selectable": false, + "backup": true, + "shared_with_other_lxc": false, + "source_path": null, + "source_path_prompt": null + }, + { + "id": "worker-volume-2", + "service": "worker", + "container_path": "/templates", + "mode": "host-bind", + "user_selectable": true, + "backup": false, + "shared_with_other_lxc": true, + "source_path": null, + "source_path_prompt": "Host directory for authentik:/templates" + } + ], + "orchestration": { + "reserve_vmids_atomically": 3, + "start_order": [ + "postgresql", + "worker", + "server" + ], + "stop_order": [ + "server", + "worker", + "postgresql" + ], + "dependency_readiness": "compose-healthcheck-then-port-or-process-fallback", + "rollback_on_failure": "remove-new-rootfs-preserve-created-persistent-volumes" + }, + "installer_inputs": { + "prompted": [ + "stack_name", + "base_vmid", + "rootfs_storage", + "persistent_data_destinations", + "frontend_bridge", + "frontend_ipv4_mode" + ], + "automatic": [ + "dependent_vmids", + "private_bridge", + "private_subnet", + "private_service_addresses", + "compose_service_aliases", + "generated_secrets", + "dependency_start_and_stop_order" + ], + "generated_secrets": [ + { + "id": "pg-pass", + "strategy": "generate-cryptographically-random-at-install", + "bindings": [ + { + "service": "postgresql", + "environment_variable": "POSTGRES_PASSWORD" + }, + { + "service": "server", + "environment_variable": "AUTHENTIK_POSTGRESQL__PASSWORD" + }, + { + "service": "worker", + "environment_variable": "AUTHENTIK_POSTGRESQL__PASSWORD" + } + ] + }, + { + "id": "authentik-secret-key", + "strategy": "generate-cryptographically-random-at-install", + "bindings": [ + { + "service": "server", + "environment_variable": "AUTHENTIK_SECRET_KEY" + }, + { + "service": "worker", + "environment_variable": "AUTHENTIK_SECRET_KEY" + } + ] + } + ] + } + }, + "first_run": { + "endpoints": [ + { + "label": "Web interface", + "scheme": "http", + "port": 9000, + "path": "/", + "source": "official-compose" + } + ], + "credentials": [ + { + "label": "Initial administrator account", + "type": "static-default", + "username": "akadmin", + "password": null, + "change_required": true, + "source": "Authentik documentation; set the akadmin password during the first-run setup" + } + ] + }, + "proxmox": { + "runtime": "native-oci-lxc", + "technology_status": "proxmox-technology-preview", + "defaults": { + "unprivileged": true, + "ostype": "auto-from-image", + "cores": 2, + "memory_mb": 1024, + "swap_mb": 512, + "rootfs_size_gb": 8, + "rootfs_storage": "local-lvm", + "volume_storage": "local-lvm", + "template_storage": "local", + "bridge": "vmbr0", + "ipv4": "dhcp", + "firewall": true, + "host_managed_network": true, + "onboot": true, + "features": [ + "nesting=1" + ], + "shutdown_timeout_seconds": 30 + }, + "image_metadata_policy": { + "entrypoint": "import-from-oci-image", + "cmd": "import-from-oci-image", + "environment": "import-image-env-then-apply-compose-overrides", + "user": "import-from-oci-image", + "working_dir": "import-from-oci-image", + "stop_signal": "import-from-oci-image" + }, + "installer_profile": { + "stack_driver": "generic-multi-lxc-stack", + "volume_preparations": [ + { + "container_path": "/data", + "remove_lost_found": true, + "owner_strategy": "mapped-application-user", + "only_when_mount_type": "managed-volume" + }, + { + "container_path": "/templates", + "remove_lost_found": true, + "owner_strategy": "mapped-application-user", + "only_when_mount_type": "managed-volume" + }, + { + "container_path": "/certs", + "remove_lost_found": true, + "owner_strategy": "mapped-application-user", + "only_when_mount_type": "managed-volume" + }, + { + "container_path": "/var/lib/postgresql/data", + "remove_lost_found": true, + "owner_strategy": "mapped-application-user", + "only_when_mount_type": "managed-volume" + } + ] + }, + "adaptations": [ + { + "id": "worker-docker-socket-removed", + "upstream_behavior": "The official Compose mounts the Docker socket into the worker for automatic deployment and management of separate outposts.", + "native_lxc_behavior": "The Docker socket mount is omitted. Authentik itself runs as server, PostgreSQL and worker in native OCI LXCs.", + "reason": "The ProxMenux native OCI stack does not run Docker Engine or provide a Docker daemon socket.", + "behavioral_impact": "Use the embedded outpost for a simple proxy-provider deployment, or deploy and manage an outpost separately.", + "validation": "passed-ms03-2026-10-30" + }, + { + "id": "postgres-healthcheck-normalized", + "upstream_behavior": "The official PostgreSQL healthcheck expands Compose environment variables in its command.", + "native_lxc_behavior": "The check uses the recipe’s fixed database and username with pg_isready against localhost.", + "reason": "The native stack healthcheck runs in the database LXC and does not evaluate Compose variable interpolation.", + "behavioral_impact": "It checks the same local database readiness before dependent services start.", + "validation": "passed-ms03-postgresql-readiness-2026-10-30" + }, + { + "id": "server-worker-shared-data", + "upstream_behavior": "Authentik server and worker both mount /data and /templates from the same Compose source directories.", + "native_lxc_behavior": "ProxMenux uses shared host directories for these two paths across the server and worker LXCs.", + "reason": "The same data and custom templates must be visible to both services; the stack driver shares named Compose sources through host paths.", + "behavioral_impact": "These shared host directories are not included in native LXC backups and need a separate backup.", + "validation": "passed-ms03-2026-10-30" + } + ], + "custom_compose_stack": true, + "custom_compose_interpolated": false, + "stack_image_entrypoints": {} + }, + "compatibility": { + "automatic_install_candidate": true, + "validated": false, + "supported_compose_keys": [ + "cap_add", + "command", + "container_name", + "cpu_shares", + "deploy", + "devices", + "entrypoint", + "environment", + "extra_hosts", + "group_add", + "healthcheck", + "hostname", + "image", + "init", + "ipc", + "labels", + "logging", + "mac_address", + "mem_limit", + "network_mode", + "networks", + "ports", + "privileged", + "restart", + "runtime", + "security_opt", + "shm_size", + "stdin_open", + "stop_grace_period", + "stop_signal", + "sysctls", + "tty", + "ulimits", + "user", + "volumes", + "working_dir" + ], + "untranslated_blockers": [], + "policy": "Official Authentik Compose translated to three native OCI LXCs. The Docker socket mount is deliberately removed; the embedded outpost remains available." + }, + "validation": { + "schema": "passed-local-2026-10-30", + "image_pull": "passed-ms03-skopeo-and-OCI-integrity-server-worker-postgresql-2026-10-30", + "default_install": "passed-ms03-three-LXCs-started-PostgreSQL-healthy-2026-10-30", + "advanced_install": "pending", + "credentials": "pending-user-first-run-and-login", + "web_ui": "pending-user-first-run-and-login", + "storage_modes": "pending", + "restart_persistence": "pending", + "backup_restore": "pending", + "update_preserves_data": "pending", + "rollback": "pending", + "modification": "pending", + "removal": "pending", + "oci_engine_tests": "pending", + "offline_i18n_tests": "pending", + "runtime_architecture": "amd64-ms03; Skopeo manifests include amd64-and-arm64", + "database_health": "passed-pg_isready-2026-10-30" + }, + "lifecycle": { + "update_strategy": "resolve-selected-tag-and-architecture-digest-then-apply-reviewed-native-lxc-update", + "registry_state": { + "resolved_architecture": null, + "resolved_digest": null, + "image_version_label": "2026.8.5", + "image_created": null + }, + "change_detection": "compare-compose-sha256-and-resolved-image-digest", + "automatic_unattended_updates": false, + "dependency_lifecycle": { + "implementation": "proxmox-hookscript", + "trigger": "main-lxc-pre-start", + "waits_for_dependency_healthchecks": true, + "stops_dependencies_with_main": false + } + } +} diff --git a/oci/catalog/curated/authentik.json b/oci/catalog/curated/authentik.json new file mode 100644 index 00000000..d466d36f --- /dev/null +++ b/oci/catalog/curated/authentik.json @@ -0,0 +1,645 @@ +{ + "schema_version": "0.5.0", + "kind": "proxmenux.oci-template", + "id": "image-authentik", + "status": "generated-unvalidated", + "catalog_ui": { + "title": { + "en_US": "Authentik" + }, + "tagline": { + "en_US": "Identity provider for single sign-on and access control" + }, + "description": { + "en_US": "Authentik is an open-source identity provider and access platform. This recipe follows the official three-service deployment with PostgreSQL, the server and a worker, each in its own Proxmox OCI LXC." + }, + "category": "security", + "category_label": "Authentication & Security", + "author": "Authentik", + "developer": "Authentik", + "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/authentik.webp", + "thumbnail": null, + "screenshots": [], + "architectures": [ + "amd64", + "arm64" + ], + "launch": { + "scheme": "http", + "port": 9000, + "path": "/" + }, + "website": "https://goauthentik.io/", + "documentation": "https://docs.goauthentik.io/install-config/install/docker-compose/", + "repository": "https://github.com/goauthentik/authentik", + "tips": [ + "The server and worker share /data and /templates through host directories; those directories are not included in Proxmox CT backups.", + "The worker Docker-socket mount is omitted. Use the embedded outpost or deploy and manage a separate outpost manually." + ], + "mini_changelog": [], + "display_version": "2026.8.5", + "updated_at": "2026-10-21" + }, + "source": { + "provider": "goauthentik", + "repository": "https://github.com/goauthentik/authentik", + "default_branch": "main", + "revision": "4ec0d19b98da3c329781894cd550da95d3033954", + "readme_raw_url": "https://goauthentik.io/version/2026.8/lifecycle/container/compose.yml", + "image_repository_url": "https://github.com/goauthentik/authentik/pkgs/container/server", + "readme_pushed_at": "2026-10-21T12:57:59Z", + "compose_sha256": "a3e138b5a85844d15c4a7ba6339997e43c0479d9d198582c67dd19523aadc694", + "generated_at": "2026-10-30T18:09:01+00:00" + }, + "container_contract": { + "service_name": "server", + "container_name": "authentik-server", + "image": { + "reference": "ghcr.io/goauthentik/server:2026.8.5", + "registry": "ghcr.io", + "repository": "ghcr.io/goauthentik/server", + "tag": "2026.8.5", + "digest": null, + "pull_policy": "resolve-written-reference-to-architecture-digest-at-install" + }, + "environment": [ + { + "name": "AUTHENTIK_POSTGRESQL__HOST", + "example": "postgresql", + "required": true, + "sensitive": false, + "source": "docker-compose" + }, + { + "name": "AUTHENTIK_POSTGRESQL__NAME", + "example": "authentik", + "required": true, + "sensitive": false, + "source": "docker-compose" + }, + { + "name": "AUTHENTIK_POSTGRESQL__PASSWORD", + "example": "${GENERATED_GENERATED_PG_PASS}", + "required": true, + "sensitive": true, + "source": "docker-compose" + }, + { + "name": "AUTHENTIK_POSTGRESQL__USER", + "example": "authentik", + "required": true, + "sensitive": false, + "source": "docker-compose" + }, + { + "name": "AUTHENTIK_SECRET_KEY", + "example": "${GENERATED_GENERATED_AUTHENTIK_SECRET_KEY}", + "required": true, + "sensitive": true, + "source": "docker-compose" + } + ], + "volumes": [ + { + "id": "volume-0", + "container_path": "/data", + "compose_source_example": "./data", + "read_only": false, + "required": true, + "installation_choice": [ + "host-bind" + ], + "default": "host-bind", + "managed_volume": { + "backup": false, + "default_size_gb": 8 + } + }, + { + "id": "volume-1", + "container_path": "/templates", + "compose_source_example": "./custom-templates", + "read_only": false, + "required": true, + "installation_choice": [ + "host-bind" + ], + "default": "host-bind", + "managed_volume": { + "backup": false, + "default_size_gb": 8 + } + } + ], + "ports": [ + { + "container_port": 9000, + "published_example": 9000, + "protocol": "tcp", + "required": true, + "proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat" + }, + { + "container_port": 9443, + "published_example": 9443, + "protocol": "tcp", + "required": true, + "proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat" + } + ], + "related_services": [ + { + "name": "postgresql", + "image": "docker.io/library/postgres:16-alpine" + }, + { + "name": "worker", + "image": "ghcr.io/goauthentik/server:2026.8.5" + } + ], + "restart": "unless-stopped", + "stop_grace_period": null, + "original_compose": "name: authentik\nservices:\n postgresql:\n environment:\n POSTGRES_DB: authentik\n POSTGRES_PASSWORD: ${GENERATED_PG_PASS}\n POSTGRES_USER: authentik\n healthcheck:\n interval: 30s\n retries: 5\n start_period: 20s\n test:\n - CMD-SHELL\n - pg_isready -h 127.0.0.1 -d authentik -U authentik\n timeout: 5s\n image: docker.io/library/postgres:16-alpine\n restart: unless-stopped\n volumes:\n - read_only: false\n source: database\n target: /var/lib/postgresql/data\n server:\n command: server\n depends_on:\n postgresql:\n condition: service_healthy\n environment:\n AUTHENTIK_POSTGRESQL__HOST: postgresql\n AUTHENTIK_POSTGRESQL__NAME: authentik\n AUTHENTIK_POSTGRESQL__PASSWORD: ${GENERATED_PG_PASS}\n AUTHENTIK_POSTGRESQL__USER: authentik\n AUTHENTIK_SECRET_KEY: ${GENERATED_AUTHENTIK_SECRET_KEY}\n image: ghcr.io/goauthentik/server:2026.8.5\n ports:\n - 9000:9000\n - 9443:9443\n restart: unless-stopped\n shm_size: 512mb\n volumes:\n - read_only: false\n source: ./data\n target: /data\n - read_only: false\n source: ./custom-templates\n target: /templates\n worker:\n command: worker\n depends_on:\n postgresql:\n condition: service_healthy\n environment:\n AUTHENTIK_POSTGRESQL__HOST: postgresql\n AUTHENTIK_POSTGRESQL__NAME: authentik\n AUTHENTIK_POSTGRESQL__PASSWORD: ${GENERATED_PG_PASS}\n AUTHENTIK_POSTGRESQL__USER: authentik\n AUTHENTIK_SECRET_KEY: ${GENERATED_AUTHENTIK_SECRET_KEY}\n image: ghcr.io/goauthentik/server:2026.8.5\n restart: unless-stopped\n shm_size: 512mb\n user: root\n volumes:\n - read_only: false\n source: ./data\n target: /data\n - read_only: false\n source: ./certs\n target: /certs\n - read_only: false\n source: ./custom-templates\n target: /templates\nvolumes:\n database: {}\n" + }, + "compose_stack": { + "project_name": "authentik", + "deployment_model": "one-native-oci-lxc-per-compose-service", + "user_experience": "single-application-install", + "main_service": "server", + "service_count": 3, + "services": [ + { + "name": "postgresql", + "image": "docker.io/library/postgres:16-alpine", + "is_main": false, + "role": "dependency", + "vmid_offset": 1, + "depends_on": [], + "frontend_network": false, + "private_network": true, + "compose": { + "environment": { + "POSTGRES_DB": "authentik", + "POSTGRES_PASSWORD": "${GENERATED_PG_PASS}", + "POSTGRES_USER": "authentik" + }, + "healthcheck": { + "interval": "30s", + "retries": 5, + "start_period": "20s", + "test": [ + "CMD-SHELL", + "pg_isready -h 127.0.0.1 -d authentik -U authentik" + ], + "timeout": "5s" + }, + "image": "docker.io/library/postgres:16-alpine", + "restart": "unless-stopped", + "volumes": [ + { + "source": "database", + "target": "/var/lib/postgresql/data", + "read_only": false + } + ] + } + }, + { + "name": "worker", + "image": "ghcr.io/goauthentik/server:2026.8.5", + "is_main": false, + "role": "dependency", + "vmid_offset": 2, + "depends_on": [ + "postgresql" + ], + "frontend_network": false, + "private_network": true, + "compose": { + "command": "worker", + "depends_on": { + "postgresql": { + "condition": "service_healthy" + } + }, + "environment": { + "AUTHENTIK_POSTGRESQL__HOST": "postgresql", + "AUTHENTIK_POSTGRESQL__NAME": "authentik", + "AUTHENTIK_POSTGRESQL__PASSWORD": "${GENERATED_PG_PASS}", + "AUTHENTIK_POSTGRESQL__USER": "authentik", + "AUTHENTIK_SECRET_KEY": "${GENERATED_AUTHENTIK_SECRET_KEY}" + }, + "image": "ghcr.io/goauthentik/server:2026.8.5", + "restart": "unless-stopped", + "shm_size": "512mb", + "user": "root", + "volumes": [ + { + "source": "./data", + "target": "/data", + "read_only": false + }, + { + "source": "./certs", + "target": "/certs", + "read_only": false + }, + { + "source": "./custom-templates", + "target": "/templates", + "read_only": false + } + ] + } + }, + { + "name": "server", + "image": "ghcr.io/goauthentik/server:2026.8.5", + "is_main": true, + "role": "frontend", + "vmid_offset": 0, + "depends_on": [ + "postgresql" + ], + "frontend_network": true, + "private_network": true, + "compose": { + "command": "server", + "depends_on": { + "postgresql": { + "condition": "service_healthy" + } + }, + "environment": { + "AUTHENTIK_POSTGRESQL__HOST": "postgresql", + "AUTHENTIK_POSTGRESQL__NAME": "authentik", + "AUTHENTIK_POSTGRESQL__PASSWORD": "${GENERATED_PG_PASS}", + "AUTHENTIK_POSTGRESQL__USER": "authentik", + "AUTHENTIK_SECRET_KEY": "${GENERATED_AUTHENTIK_SECRET_KEY}" + }, + "image": "ghcr.io/goauthentik/server:2026.8.5", + "ports": [ + "9000:9000", + "9443:9443" + ], + "restart": "unless-stopped", + "shm_size": "512mb", + "volumes": [ + { + "source": "./data", + "target": "/data", + "read_only": false + }, + { + "source": "./custom-templates", + "target": "/templates", + "read_only": false + } + ] + } + } + ], + "top_level": { + "name": "authentik", + "volumes": { + "database": {} + } + }, + "networking": { + "frontend": "selected-proxmox-bridge", + "private_required": true, + "private_creation": "automatic-create-if-missing", + "private_address_allocation": "automatic-static-address-per-service", + "service_discovery": "private-addresses-with-compose-service-host-aliases", + "dependency_external_access": "disabled-unless-service-publishes-ports", + "prompt_user_for_private_network": false + }, + "storage": [ + { + "id": "postgresql-volume-0", + "service": "postgresql", + "container_path": "/var/lib/postgresql/data", + "mode": "managed-volume", + "user_selectable": false, + "backup": true, + "shared_with_other_lxc": false, + "source_path": null, + "source_path_prompt": null + }, + { + "id": "server-volume-0", + "service": "server", + "container_path": "/data", + "mode": "host-bind", + "user_selectable": true, + "backup": false, + "shared_with_other_lxc": true, + "source_path": null, + "source_path_prompt": "Host directory for authentik:/data" + }, + { + "id": "server-volume-1", + "service": "server", + "container_path": "/templates", + "mode": "host-bind", + "user_selectable": true, + "backup": false, + "shared_with_other_lxc": true, + "source_path": null, + "source_path_prompt": "Host directory for authentik:/templates" + }, + { + "id": "worker-volume-0", + "service": "worker", + "container_path": "/data", + "mode": "host-bind", + "user_selectable": true, + "backup": false, + "shared_with_other_lxc": true, + "source_path": null, + "source_path_prompt": "Host directory for authentik:/data" + }, + { + "id": "worker-volume-1", + "service": "worker", + "container_path": "/certs", + "mode": "managed-volume", + "user_selectable": false, + "backup": true, + "shared_with_other_lxc": false, + "source_path": null, + "source_path_prompt": null + }, + { + "id": "worker-volume-2", + "service": "worker", + "container_path": "/templates", + "mode": "host-bind", + "user_selectable": true, + "backup": false, + "shared_with_other_lxc": true, + "source_path": null, + "source_path_prompt": "Host directory for authentik:/templates" + } + ], + "orchestration": { + "reserve_vmids_atomically": 3, + "start_order": [ + "postgresql", + "worker", + "server" + ], + "stop_order": [ + "server", + "worker", + "postgresql" + ], + "dependency_readiness": "compose-healthcheck-then-port-or-process-fallback", + "rollback_on_failure": "remove-new-rootfs-preserve-created-persistent-volumes" + }, + "installer_inputs": { + "prompted": [ + "stack_name", + "base_vmid", + "rootfs_storage", + "persistent_data_destinations", + "frontend_bridge", + "frontend_ipv4_mode" + ], + "automatic": [ + "dependent_vmids", + "private_bridge", + "private_subnet", + "private_service_addresses", + "compose_service_aliases", + "generated_secrets", + "dependency_start_and_stop_order" + ], + "generated_secrets": [ + { + "id": "pg-pass", + "strategy": "generate-cryptographically-random-at-install", + "bindings": [ + { + "service": "postgresql", + "environment_variable": "POSTGRES_PASSWORD" + }, + { + "service": "server", + "environment_variable": "AUTHENTIK_POSTGRESQL__PASSWORD" + }, + { + "service": "worker", + "environment_variable": "AUTHENTIK_POSTGRESQL__PASSWORD" + } + ] + }, + { + "id": "authentik-secret-key", + "strategy": "generate-cryptographically-random-at-install", + "bindings": [ + { + "service": "server", + "environment_variable": "AUTHENTIK_SECRET_KEY" + }, + { + "service": "worker", + "environment_variable": "AUTHENTIK_SECRET_KEY" + } + ] + } + ] + } + }, + "first_run": { + "endpoints": [ + { + "label": "Web interface", + "scheme": "http", + "port": 9000, + "path": "/", + "source": "official-compose" + } + ], + "credentials": [ + { + "label": "Initial administrator account", + "type": "static-default", + "username": "akadmin", + "password": null, + "change_required": true, + "source": "Authentik documentation; set the akadmin password during the first-run setup" + } + ] + }, + "proxmox": { + "runtime": "native-oci-lxc", + "technology_status": "proxmox-technology-preview", + "defaults": { + "unprivileged": true, + "ostype": "auto-from-image", + "cores": 2, + "memory_mb": 1024, + "swap_mb": 512, + "rootfs_size_gb": 8, + "rootfs_storage": "local-lvm", + "volume_storage": "local-lvm", + "template_storage": "local", + "bridge": "vmbr0", + "ipv4": "dhcp", + "firewall": true, + "host_managed_network": true, + "onboot": true, + "features": [ + "nesting=1" + ], + "shutdown_timeout_seconds": 30 + }, + "image_metadata_policy": { + "entrypoint": "import-from-oci-image", + "cmd": "import-from-oci-image", + "environment": "import-image-env-then-apply-compose-overrides", + "user": "import-from-oci-image", + "working_dir": "import-from-oci-image", + "stop_signal": "import-from-oci-image" + }, + "installer_profile": { + "stack_driver": "generic-multi-lxc-stack", + "volume_preparations": [ + { + "container_path": "/data", + "remove_lost_found": true, + "owner_strategy": "mapped-application-user", + "only_when_mount_type": "managed-volume" + }, + { + "container_path": "/templates", + "remove_lost_found": true, + "owner_strategy": "mapped-application-user", + "only_when_mount_type": "managed-volume" + }, + { + "container_path": "/certs", + "remove_lost_found": true, + "owner_strategy": "mapped-application-user", + "only_when_mount_type": "managed-volume" + }, + { + "container_path": "/var/lib/postgresql/data", + "remove_lost_found": true, + "owner_strategy": "mapped-application-user", + "only_when_mount_type": "managed-volume" + } + ] + }, + "adaptations": [ + { + "id": "worker-docker-socket-removed", + "upstream_behavior": "The official Compose mounts the Docker socket into the worker for automatic deployment and management of separate outposts.", + "native_lxc_behavior": "The Docker socket mount is omitted. Authentik itself runs as server, PostgreSQL and worker in native OCI LXCs.", + "reason": "The ProxMenux native OCI stack does not run Docker Engine or provide a Docker daemon socket.", + "behavioral_impact": "Use the embedded outpost for a simple proxy-provider deployment, or deploy and manage an outpost separately.", + "validation": "passed-ms03-2026-10-30" + }, + { + "id": "postgres-healthcheck-normalized", + "upstream_behavior": "The official PostgreSQL healthcheck expands Compose environment variables in its command.", + "native_lxc_behavior": "The check uses the recipe’s fixed database and username with pg_isready against localhost.", + "reason": "The native stack healthcheck runs in the database LXC and does not evaluate Compose variable interpolation.", + "behavioral_impact": "It checks the same local database readiness before dependent services start.", + "validation": "passed-ms03-postgresql-readiness-2026-10-30" + }, + { + "id": "server-worker-shared-data", + "upstream_behavior": "Authentik server and worker both mount /data and /templates from the same Compose source directories.", + "native_lxc_behavior": "ProxMenux uses shared host directories for these two paths across the server and worker LXCs.", + "reason": "The same data and custom templates must be visible to both services; the stack driver shares named Compose sources through host paths.", + "behavioral_impact": "These shared host directories are not included in native LXC backups and need a separate backup.", + "validation": "passed-ms03-2026-10-30" + } + ], + "custom_compose_stack": true, + "custom_compose_interpolated": false, + "stack_image_entrypoints": {} + }, + "compatibility": { + "automatic_install_candidate": true, + "validated": false, + "supported_compose_keys": [ + "cap_add", + "command", + "container_name", + "cpu_shares", + "deploy", + "devices", + "entrypoint", + "environment", + "extra_hosts", + "group_add", + "healthcheck", + "hostname", + "image", + "init", + "ipc", + "labels", + "logging", + "mac_address", + "mem_limit", + "network_mode", + "networks", + "ports", + "privileged", + "restart", + "runtime", + "security_opt", + "shm_size", + "stdin_open", + "stop_grace_period", + "stop_signal", + "sysctls", + "tty", + "ulimits", + "user", + "volumes", + "working_dir" + ], + "untranslated_blockers": [], + "policy": "Official Authentik Compose translated to three native OCI LXCs. The Docker socket mount is deliberately removed; the embedded outpost remains available." + }, + "validation": { + "schema": "passed-local-2026-10-30", + "image_pull": "passed-ms03-skopeo-and-OCI-integrity-server-worker-postgresql-2026-10-30", + "default_install": "passed-ms03-three-LXCs-started-PostgreSQL-healthy-2026-10-30", + "advanced_install": "pending", + "credentials": "pending-user-first-run-and-login", + "web_ui": "pending-user-first-run-and-login", + "storage_modes": "pending", + "restart_persistence": "pending", + "backup_restore": "pending", + "update_preserves_data": "pending", + "rollback": "pending", + "modification": "pending", + "removal": "pending", + "oci_engine_tests": "pending", + "offline_i18n_tests": "pending", + "runtime_architecture": "amd64-ms03; Skopeo manifests include amd64-and-arm64", + "database_health": "passed-pg_isready-2026-10-30" + }, + "lifecycle": { + "update_strategy": "resolve-selected-tag-and-architecture-digest-then-apply-reviewed-native-lxc-update", + "registry_state": { + "resolved_architecture": null, + "resolved_digest": null, + "image_version_label": "2026.8.5", + "image_created": null + }, + "change_detection": "compare-compose-sha256-and-resolved-image-digest", + "automatic_unattended_updates": false, + "dependency_lifecycle": { + "implementation": "proxmox-hookscript", + "trigger": "main-lxc-pre-start", + "waits_for_dependency_healthchecks": true, + "stops_dependencies_with_main": false + } + } +} diff --git a/oci/catalog/index.json b/oci/catalog/index.json index 1b6d7b4f..0457de05 100644 --- a/oci/catalog/index.json +++ b/oci/catalog/index.json @@ -2,7 +2,7 @@ "schema_version": "0.2.0", "kind": "proxmenux.oci-catalog-index", "provider": "multiple-container-images", - "generated_at": "2026-10-26T08:05:54+00:00", + "generated_at": "2026-10-30T18:13:36+00:00", "applications": [ { "id": "2fauth", @@ -752,6 +752,39 @@ "hidden": false, "template_id": "image-audiobookshelf" }, + { + "id": "authentik", + "provider": "goauthentik", + "template_family": "curated-profile", + "title": "Authentik", + "repository": "https://github.com/goauthentik/authentik", + "description": "Authentik is an open-source identity provider and access platform. This recipe follows the official three-service deployment with PostgreSQL, the server and a worker, each in its own Proxmox OCI LXC.", + "website": "https://goauthentik.io/", + "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/authentik.webp", + "architectures": [ + "amd64", + "arm64" + ], + "updated_at": "2026-10-21", + "main_image": "ghcr.io/goauthentik/server:2026.8.5", + "category": "security", + "category_label": "Authentication & Security", + "replaces_discovered_ids": [], + "curated_path": "catalog/curated/authentik.json", + "template_id": "image-authentik", + "template": "apps/authentik.json", + "template_status": "generated-unvalidated", + "hidden": false, + "automatic_install_candidate": true, + "untranslated_blockers": [], + "requires_privileged_lxc": false, + "optional_privileged_lxc": false, + "requires_host_pid_namespace": false, + "requires_relaxed_confinement": false, + "optional_relaxed_confinement": false, + "requires_security_confirmation": false, + "multi_container": true + }, { "id": "autobrr", "source_app_id": "autobrr", @@ -18976,7 +19009,7 @@ "skipped": [] }, "curated_profiles": { - "included_count": 17 + "included_count": 18 } } } diff --git a/oci/tests/test_authentik_recipe.py b/oci/tests/test_authentik_recipe.py new file mode 100644 index 00000000..15102b1b --- /dev/null +++ b/oci/tests/test_authentik_recipe.py @@ -0,0 +1,143 @@ +"""Authentik keeps the vendor's three-service deployment as native OCI LXCs.""" + +import json +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] + + +class AuthentikRecipeTests(unittest.TestCase): + def setUp(self): + self.curated = json.loads( + (ROOT / "catalog/curated/authentik.json").read_text(encoding="utf-8") + ) + self.generated = json.loads( + (ROOT / "catalog/apps/authentik.json").read_text(encoding="utf-8") + ) + self.services = { + service["name"]: service + for service in self.curated["compose_stack"]["services"] + } + + def test_generated_catalog_copy_matches_curated_profile(self): + self.assertEqual(self.curated, self.generated) + + def test_official_three_service_stack_uses_pinned_images_and_order(self): + self.assertEqual(set(self.services), {"postgresql", "server", "worker"}) + self.assertEqual(self.curated["compose_stack"]["main_service"], "server") + self.assertEqual( + self.services["server"]["image"], "ghcr.io/goauthentik/server:2026.8.5" + ) + self.assertEqual( + self.services["worker"]["image"], "ghcr.io/goauthentik/server:2026.8.5" + ) + self.assertEqual( + self.services["postgresql"]["image"], + "docker.io/library/postgres:16-alpine", + ) + self.assertEqual(self.services["server"]["depends_on"], ["postgresql"]) + self.assertEqual(self.services["worker"]["depends_on"], ["postgresql"]) + + def test_generated_database_and_application_secrets_are_shared_correctly(self): + services = { + name: service["compose"].get("environment", {}) + for name, service in self.services.items() + } + self.assertEqual( + services["postgresql"]["POSTGRES_PASSWORD"], "${GENERATED_PG_PASS}" + ) + for name in ("server", "worker"): + self.assertEqual( + services[name]["AUTHENTIK_POSTGRESQL__PASSWORD"], + "${GENERATED_PG_PASS}", + ) + self.assertEqual( + services[name]["AUTHENTIK_SECRET_KEY"], + "${GENERATED_AUTHENTIK_SECRET_KEY}", + ) + generated = self.curated["compose_stack"]["installer_inputs"][ + "generated_secrets" + ] + self.assertEqual({secret["id"] for secret in generated}, { + "pg-pass", + "authentik-secret-key", + }) + + def test_persistent_mounts_and_database_healthcheck_are_preserved(self): + mounts = { + name: service["compose"].get("volumes", []) + for name, service in self.services.items() + } + self.assertIn( + {"source": "database", "target": "/var/lib/postgresql/data", "read_only": False}, + mounts["postgresql"], + ) + self.assertIn( + {"source": "./data", "target": "/data", "read_only": False}, + mounts["server"], + ) + self.assertIn( + {"source": "./data", "target": "/data", "read_only": False}, + mounts["worker"], + ) + self.assertIn( + {"source": "./custom-templates", "target": "/templates", "read_only": False}, + mounts["server"], + ) + self.assertIn( + {"source": "./custom-templates", "target": "/templates", "read_only": False}, + mounts["worker"], + ) + self.assertIn( + {"source": "./certs", "target": "/certs", "read_only": False}, + mounts["worker"], + ) + check = self.services["postgresql"]["compose"]["healthcheck"]["test"] + self.assertEqual( + check, + ["CMD-SHELL", "pg_isready -h 127.0.0.1 -d authentik -U authentik"], + ) + + def test_cross_lxc_data_and_templates_are_host_shared_and_backed_up_separately(self): + shared = [ + item + for item in self.curated["compose_stack"]["storage"] + if item["container_path"] in ("/data", "/templates") + ] + self.assertEqual(len(shared), 4) + self.assertTrue(all(item["mode"] == "host-bind" for item in shared)) + self.assertTrue(all(item["shared_with_other_lxc"] for item in shared)) + self.assertTrue(all(item["backup"] is False for item in shared)) + self.assertTrue( + all( + volume["default"] == "host-bind" + and volume["managed_volume"]["backup"] is False + for volume in self.curated["container_contract"]["volumes"] + ) + ) + + def test_docker_socket_is_not_mounted_and_both_web_ports_are_exposed(self): + all_mounts = [ + mount + for service in self.services.values() + for mount in service["compose"].get("volumes", []) + ] + self.assertFalse(any("docker.sock" in mount for mount in all_mounts)) + self.assertEqual( + self.services["server"]["compose"]["ports"], + ["9000:9000", "9443:9443"], + ) + + def test_first_run_discloses_the_initial_admin_setup_without_faking_a_password(self): + first_run = self.curated["first_run"] + self.assertEqual(first_run["endpoints"][0]["port"], 9000) + credential = first_run["credentials"][0] + self.assertEqual(credential["username"], "akadmin") + self.assertIsNone(credential["password"]) + self.assertTrue(credential["change_required"]) + + +if __name__ == "__main__": + unittest.main()