diff --git a/apps/kaneo/2.9.9/.env.sample b/apps/kaneo/2.9.9/.env.sample new file mode 100644 index 000000000..a92b6d019 --- /dev/null +++ b/apps/kaneo/2.9.9/.env.sample @@ -0,0 +1,10 @@ +CONTAINER_NAME=kaneo +PANEL_APP_BIND_ADDRESS=127.0.0.1 +PANEL_APP_PORT_HTTP=5173 +KANEO_CLIENT_URL=http://localhost:5173 +DISABLE_REGISTRATION=false +POSTGRES_DB=kaneo +POSTGRES_USER=kaneo +POSTGRES_PASSWORD=generate +AUTH_SECRET=generate +APP_DATA_DIR=./data diff --git a/apps/kaneo/2.9.9/data.yml b/apps/kaneo/2.9.9/data.yml new file mode 100644 index 000000000..0fc03fec2 --- /dev/null +++ b/apps/kaneo/2.9.9/data.yml @@ -0,0 +1,156 @@ +additionalProperties: + formFields: + - default: 127.0.0.1 + edit: true + envKey: PANEL_APP_BIND_ADDRESS + labelEn: Bind Address + labelZh: 绑定地址 + label: + en: Bind Address + zh: 绑定地址 + zh-Hant: 綁定位址 + ja: バインドアドレス + ko: 바인드 주소 + ru: Адрес привязки + ms: Alamat ikatan + pt-br: Endereco de vinculacao + required: true + type: text + - default: 5173 + edit: true + envKey: PANEL_APP_PORT_HTTP + labelEn: HTTP Port + labelZh: HTTP 端口 + label: + en: HTTP Port + zh: HTTP 端口 + zh-Hant: HTTP 連接埠 + ja: HTTP ポート + ko: HTTP 포트 + ru: HTTP-порт + ms: Port HTTP + pt-br: Porta HTTP + required: true + rule: paramPort + type: number + - default: http://localhost:5173 + edit: true + envKey: KANEO_CLIENT_URL + labelEn: Public URL + labelZh: 公开访问地址 + label: + en: Public URL + zh: 公开访问地址 + zh-Hant: 公開存取網址 + ja: 公開 URL + ko: 공개 URL + ru: Публичный URL + ms: URL awam + pt-br: URL publica + required: true + type: text + - default: "false" + edit: true + envKey: DISABLE_REGISTRATION + labelEn: Disable Registration + labelZh: 禁止新用户注册 + label: + en: Disable Registration + zh: 禁止新用户注册 + zh-Hant: 停用新使用者註冊 + ja: 新規登録を無効化 + ko: 신규 등록 비활성화 + ru: Отключить регистрацию + ms: Nyahdayakan pendaftaran + pt-br: Desativar cadastro + required: true + type: select + values: + - label: "false" + value: "false" + - label: "true" + value: "true" + - default: kaneo + edit: true + envKey: POSTGRES_DB + labelEn: Database Name + labelZh: 数据库名称 + label: + en: Database Name + zh: 数据库名称 + zh-Hant: 資料庫名稱 + ja: データベース名 + ko: 데이터베이스 이름 + ru: Имя базы данных + ms: Nama pangkalan data + pt-br: Nome do banco de dados + random: true + required: true + rule: paramCommon + type: text + - default: kaneo + edit: true + envKey: POSTGRES_USER + labelEn: Database User + labelZh: 数据库用户 + label: + en: Database User + zh: 数据库用户 + zh-Hant: 資料庫使用者 + ja: データベースユーザー + ko: 데이터베이스 사용자 + ru: Пользователь базы данных + ms: Pengguna pangkalan data + pt-br: Usuario do banco de dados + random: true + required: true + rule: paramCommon + type: text + - default: generate + edit: true + envKey: POSTGRES_PASSWORD + labelEn: Database Password + labelZh: 数据库密码 + label: + en: Database Password + zh: 数据库密码 + zh-Hant: 資料庫密碼 + ja: データベースパスワード + ko: 데이터베이스 비밀번호 + ru: Пароль базы данных + ms: Kata laluan pangkalan data + pt-br: Senha do banco de dados + required: true + type: password + - default: generate + edit: true + envKey: AUTH_SECRET + labelEn: Authentication Secret + labelZh: 认证密钥 + label: + en: Authentication Secret + zh: 认证密钥 + zh-Hant: 驗證密鑰 + ja: 認証シークレット + ko: 인증 비밀 키 + ru: Секрет аутентификации + ms: Rahsia pengesahan + pt-br: Segredo de autenticacao + required: true + type: password + - default: ./data + edit: true + envKey: APP_DATA_DIR + labelEn: Data Directory + labelZh: 数据目录 + label: + en: Data Directory + zh: 数据目录 + zh-Hant: 資料目錄 + ja: データディレクトリ + ko: 데이터 디렉터리 + ru: Каталог данных + ms: Direktori data + pt-br: Diretorio de dados + required: true + type: text diff --git a/apps/kaneo/2.9.9/docker-compose.yml b/apps/kaneo/2.9.9/docker-compose.yml new file mode 100644 index 000000000..1f3efd73e --- /dev/null +++ b/apps/kaneo/2.9.9/docker-compose.yml @@ -0,0 +1,73 @@ +services: + kaneo-postgres: + image: postgres:16-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777 + container_name: ${CONTAINER_NAME}-postgres + restart: unless-stopped + user: "70:70" + networks: + - kaneo-internal + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - "${APP_DATA_DIR}/postgres:/var/lib/postgresql/data" + read_only: true + tmpfs: + - /tmp:mode=1777,size=64m + - /var/run/postgresql:uid=70,gid=70,mode=0755,size=16m + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + healthcheck: + test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""] + interval: 10s + timeout: 5s + retries: 10 + start_period: 20s + labels: + createdBy: "Apps" + + kaneo: + image: ghcr.io/usekaneo/kaneo:2.9.9@sha256:d83479f41d437d5c35656f811f345a50357588422fc8d504cfde002756da688e + container_name: ${CONTAINER_NAME} + restart: unless-stopped + user: "1001:1001" + depends_on: + kaneo-postgres: + condition: service_healthy + networks: + - kaneo-internal + - 1panel-network + ports: + - "${PANEL_APP_BIND_ADDRESS}:${PANEL_APP_PORT_HTTP}:5173" + environment: + KANEO_CLIENT_URL: ${KANEO_CLIENT_URL} + POSTGRES_HOST: kaneo-postgres + POSTGRES_PORT: "5432" + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + AUTH_SECRET: ${AUTH_SECRET} + DISABLE_REGISTRATION: ${DISABLE_REGISTRATION} + tmpfs: + - /tmp:mode=1777,size=64m + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:5173/api/health"] + interval: 15s + timeout: 5s + retries: 8 + start_period: 60s + labels: + createdBy: "Apps" + +networks: + kaneo-internal: + internal: true + 1panel-network: + external: true diff --git a/apps/kaneo/2.9.9/scripts/init.sh b/apps/kaneo/2.9.9/scripts/init.sh new file mode 100755 index 000000000..65b615bc8 --- /dev/null +++ b/apps/kaneo/2.9.9/scripts/init.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}" + +fail() { + printf '%s\n' "$1" >&2 + exit 1 +} + +read_env_value() { + local key="$1" + local value + value="$(sed -n "s/^${key}=//p" "$ENV_FILE" | tail -n 1)" + case "$value" in + \"*\") value="${value#\"}"; value="${value%\"}" ;; + \'*\') value="${value#\'}"; value="${value%\'}" ;; + esac + printf '%s\n' "$value" +} + +set_env_value() { + local key="$1" + local value="$2" + local temp_file + temp_file="$(mktemp "${ROOT_DIR}/.kaneo-env.tmp.XXXXXX")" + awk -v key="$key" -v value="$value" ' + BEGIN { written = 0 } + $0 ~ "^" key "=" { + if (!written) { + print key "=" value + written = 1 + } + next + } + { print } + END { if (!written) print key "=" value } + ' "$ENV_FILE" > "$temp_file" + chmod 600 "$temp_file" + mv -f -- "$temp_file" "$ENV_FILE" +} + +validate_ipv4() { + local key="$1" + local value="$2" + local octet + local -a octets + [[ "$value" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || fail "$key must be an IPv4 address" + IFS=. read -r -a octets <<< "$value" + for octet in "${octets[@]}"; do + ((10#$octet <= 255)) || fail "$key contains an invalid IPv4 octet" + done +} + +validate_port() { + local key="$1" + local value="$2" + [[ "$value" =~ ^[0-9]+$ ]] || fail "$key must be an integer" + ((10#$value >= 1 && 10#$value <= 65535)) || fail "$key must be between 1 and 65535" +} + +generate_alphanumeric() { + local length="$1" + local material + material="$(openssl rand -base64 96 | tr -dc A-Za-z0-9)" + [[ ${#material} -ge $length ]] || fail "unable to generate sufficient random material" + printf '%s\n' "${material:0:length}" +} + +[[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found" +[[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link" +[[ "$(id -u)" -eq 0 ]] || fail "Kaneo init must run as root" +command -v openssl >/dev/null 2>&1 || fail "openssl is required to generate credentials" + +validate_ipv4 PANEL_APP_BIND_ADDRESS "$(read_env_value PANEL_APP_BIND_ADDRESS)" +validate_port PANEL_APP_PORT_HTTP "$(read_env_value PANEL_APP_PORT_HTTP)" + +client_url="$(read_env_value KANEO_CLIENT_URL)" +[[ "$client_url" =~ ^https?://([A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?|localhost)(:([0-9]{1,5}))?$ ]] || + fail "KANEO_CLIENT_URL must be an HTTP(S) origin without a path" +[[ "$client_url" != *..* ]] || fail "KANEO_CLIENT_URL contains an invalid hostname" +if [[ -n "${BASH_REMATCH[4]:-}" ]]; then + validate_port KANEO_CLIENT_URL_PORT "${BASH_REMATCH[4]}" +fi + +disable_registration="$(read_env_value DISABLE_REGISTRATION)" +[[ "$disable_registration" == true || "$disable_registration" == false ]] || + fail "DISABLE_REGISTRATION must be true or false" + +postgres_db="$(read_env_value POSTGRES_DB)" +postgres_user="$(read_env_value POSTGRES_USER)" +[[ "$postgres_db" =~ ^[A-Za-z_][A-Za-z0-9_]{0,62}$ ]] || fail "POSTGRES_DB is invalid" +[[ "$postgres_user" =~ ^[A-Za-z_][A-Za-z0-9_]{0,62}$ ]] || fail "POSTGRES_USER is invalid" + +postgres_password="$(read_env_value POSTGRES_PASSWORD)" +if [[ -z "$postgres_password" || "$postgres_password" == generate ]]; then + postgres_password="$(generate_alphanumeric 48)" +fi +[[ "$postgres_password" =~ ^[A-Za-z0-9]{32,128}$ ]] || + fail "POSTGRES_PASSWORD must contain 32 to 128 alphanumeric characters" + +auth_secret="$(read_env_value AUTH_SECRET)" +if [[ -z "$auth_secret" || "$auth_secret" == generate ]]; then + auth_secret="$(openssl rand -hex 32)" +fi +[[ "$auth_secret" =~ ^[A-Fa-f0-9]{64,128}$ ]] || + fail "AUTH_SECRET must contain 64 to 128 hexadecimal characters" + +data_raw="$(read_env_value APP_DATA_DIR)" +[[ -n "$data_raw" && "$data_raw" != /* ]] || fail "APP_DATA_DIR must be a non-empty relative path" +case "$data_raw" in + *$'\n'* | *$'\r'* | *\\* | *:* | *'$'* | *'#'* | *'"'* | *"'"*) + fail "APP_DATA_DIR contains unsupported characters" + ;; +esac + +relative_data="${data_raw#./}" +[[ -n "$relative_data" ]] || fail "APP_DATA_DIR must not resolve to the version root" +current="$ROOT_DIR" +IFS=/ read -r -a components <<< "$relative_data" +for component in "${components[@]}"; do + [[ -n "$component" && "$component" != . && "$component" != .. ]] || fail "APP_DATA_DIR contains traversal" + current="$current/$component" + [[ ! -L "$current" ]] || fail "APP_DATA_DIR must not contain symbolic-link components" +done + +data_dir="$(realpath -m -- "$ROOT_DIR/$relative_data")" +case "$data_dir" in + "$ROOT_DIR"/*) ;; + *) fail "APP_DATA_DIR must stay inside the application version directory" ;; +esac + +install -d -m 0700 -- "$data_dir/postgres" +resolved_data="$(realpath -e -- "$data_dir")" +case "$resolved_data" in + "$ROOT_DIR"/*) ;; + *) fail "APP_DATA_DIR resolves outside the application version directory" ;; +esac +chown -R 70:70 -- "$resolved_data/postgres" +chmod 0700 -- "$resolved_data/postgres" + +set_env_value POSTGRES_PASSWORD "$postgres_password" +set_env_value AUTH_SECRET "$auth_secret" +chmod 600 "$ENV_FILE" diff --git a/apps/kaneo/2.9.9/scripts/uninstall.sh b/apps/kaneo/2.9.9/scripts/uninstall.sh new file mode 100755 index 000000000..5c199242b --- /dev/null +++ b/apps/kaneo/2.9.9/scripts/uninstall.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +if command -v docker-compose >/dev/null 2>&1; then + docker-compose down --remove-orphans +else + docker compose down --remove-orphans +fi diff --git a/apps/kaneo/2.9.9/scripts/upgrade.sh b/apps/kaneo/2.9.9/scripts/upgrade.sh new file mode 100755 index 000000000..5ec28d784 --- /dev/null +++ b/apps/kaneo/2.9.9/scripts/upgrade.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +exec "$SCRIPT_DIR/init.sh" diff --git a/apps/kaneo/README.md b/apps/kaneo/README.md new file mode 100644 index 000000000..bacb4588d --- /dev/null +++ b/apps/kaneo/README.md @@ -0,0 +1,75 @@ +# Kaneo + +## 产品介绍 + +Kaneo 是一个轻量级的开源项目管理平台,提供看板、任务、标签、评论、 +时间记录、实时更新和多种第三方集成。 + +## 主要功能 + +- 使用看板、任务、标签、评论和时间记录管理项目 +- 通过 WebSocket 实时同步项目变更 +- 支持 GitHub、Gitea、Slack、Discord、Telegram 和通用 Webhook 集成 +- 提供 API、MCP 接口和可选的对象存储图片附件 + +## 访问说明 + +- Web 端口默认只绑定 `127.0.0.1`。请在 1Panel 中配置 HTTPS 反向代理。 +- 安装时的公开访问地址必须填写完整的 HTTP(S) origin,例如 + `https://kaneo.example.com`,不要包含路径或末尾斜杠。 +- 第一个注册用户会成为实例管理员。确认管理员账号可用后,可在应用参数中 + 将“禁止新用户注册”设为 `true` 并重启应用。 + +## 安全与数据 + +- 安装脚本会一次生成数据库密码和认证密钥,并写入权限为 `0600` 的 + `.env`。升级和重启不会自动轮换。 +- Kaneo 以官方 UID/GID `1001:1001` 运行;PostgreSQL 以 `70:70` + 直接运行,不执行镜像内的 root/gosu 切换路径。 +- 两个容器都丢弃全部 Linux capabilities 并禁止权限提升。PostgreSQL + 仅连接内部 Docker 网络,不发布主机端口。 +- PostgreSQL 数据保存在安装版本目录下的 `data/postgres`。卸载不会删除 + 该目录;升级前请备份整个数据目录。 + +## Introduction + +Kaneo is a lightweight open-source project management platform with boards, +tasks, labels, comments, time tracking, live updates, and integrations. + +## Features + +- Manage projects with boards, tasks, labels, comments, and time tracking +- Synchronize project changes in real time over WebSockets +- Integrate with GitHub, Gitea, Slack, Discord, Telegram, and generic webhooks +- Use its API, MCP interface, and optional object storage for image attachments + +## Access And First Use + +- The web port binds to `127.0.0.1` by default. Publish it through the 1Panel + reverse proxy with HTTPS. +- Set Public URL to the complete HTTP(S) origin used by browsers, such as + `https://kaneo.example.com`. Do not include a path or trailing slash. +- The first registered user becomes the instance administrator. After that + account works, set Disable Registration to `true` and restart if public + signup is not required. + +## Security And Data + +- Installation generates the database password and authentication secret once + and stores them in the mode `0600` `.env`. Restart and upgrade preserve them. +- Kaneo runs as the upstream UID/GID `1001:1001`. PostgreSQL runs directly as + `70:70`, bypassing the image's root/gosu identity-switch path. +- Both services drop every Linux capability and prevent privilege escalation. + PostgreSQL is restricted to an internal Docker network and publishes no host + port. +- PostgreSQL data is stored in `data/postgres` below the installed version. + Uninstall preserves it. Back up the complete data directory before upgrades. +- Kaneo's root filesystem remains writable because the official startup script + replaces frontend runtime URLs and nginx discovery metadata in place. The + service remains non-root and can write only paths owned by its upstream user. + +## References + +- Source: +- Release: +- Self-hosting: diff --git a/apps/kaneo/data.yml b/apps/kaneo/data.yml new file mode 100644 index 000000000..5619e5eca --- /dev/null +++ b/apps/kaneo/data.yml @@ -0,0 +1,31 @@ +name: Kaneo +tags: + - 项目管理 +title: 轻量级开源项目管理平台 +description: 轻量级开源项目管理平台 +additionalProperties: + key: kaneo + name: Kaneo + tags: + - Project Management + shortDescZh: 轻量级开源项目管理平台 + shortDescEn: Lightweight open-source project management platform + description: + en: A lightweight open-source project management platform + zh: 一个轻量级开源项目管理平台 + zh-Hant: 一個輕量級開源專案管理平台 + ja: 軽量なオープンソースのプロジェクト管理プラットフォーム + ko: 가벼운 오픈 소스 프로젝트 관리 플랫폼 + ru: Легкая платформа управления проектами с открытым исходным кодом + ms: Platform pengurusan projek sumber terbuka yang ringan + pt-br: Plataforma leve de gerenciamento de projetos de codigo aberto + type: website + crossVersionUpdate: true + limit: 0 + recommend: 0 + website: https://kaneo.app/ + github: https://github.com/usekaneo/kaneo + document: https://github.com/usekaneo/kaneo#self-hosting + architectures: + - amd64 + - arm64 diff --git a/apps/kaneo/latest/.env.sample b/apps/kaneo/latest/.env.sample new file mode 100644 index 000000000..a92b6d019 --- /dev/null +++ b/apps/kaneo/latest/.env.sample @@ -0,0 +1,10 @@ +CONTAINER_NAME=kaneo +PANEL_APP_BIND_ADDRESS=127.0.0.1 +PANEL_APP_PORT_HTTP=5173 +KANEO_CLIENT_URL=http://localhost:5173 +DISABLE_REGISTRATION=false +POSTGRES_DB=kaneo +POSTGRES_USER=kaneo +POSTGRES_PASSWORD=generate +AUTH_SECRET=generate +APP_DATA_DIR=./data diff --git a/apps/kaneo/latest/data.yml b/apps/kaneo/latest/data.yml new file mode 100644 index 000000000..0fc03fec2 --- /dev/null +++ b/apps/kaneo/latest/data.yml @@ -0,0 +1,156 @@ +additionalProperties: + formFields: + - default: 127.0.0.1 + edit: true + envKey: PANEL_APP_BIND_ADDRESS + labelEn: Bind Address + labelZh: 绑定地址 + label: + en: Bind Address + zh: 绑定地址 + zh-Hant: 綁定位址 + ja: バインドアドレス + ko: 바인드 주소 + ru: Адрес привязки + ms: Alamat ikatan + pt-br: Endereco de vinculacao + required: true + type: text + - default: 5173 + edit: true + envKey: PANEL_APP_PORT_HTTP + labelEn: HTTP Port + labelZh: HTTP 端口 + label: + en: HTTP Port + zh: HTTP 端口 + zh-Hant: HTTP 連接埠 + ja: HTTP ポート + ko: HTTP 포트 + ru: HTTP-порт + ms: Port HTTP + pt-br: Porta HTTP + required: true + rule: paramPort + type: number + - default: http://localhost:5173 + edit: true + envKey: KANEO_CLIENT_URL + labelEn: Public URL + labelZh: 公开访问地址 + label: + en: Public URL + zh: 公开访问地址 + zh-Hant: 公開存取網址 + ja: 公開 URL + ko: 공개 URL + ru: Публичный URL + ms: URL awam + pt-br: URL publica + required: true + type: text + - default: "false" + edit: true + envKey: DISABLE_REGISTRATION + labelEn: Disable Registration + labelZh: 禁止新用户注册 + label: + en: Disable Registration + zh: 禁止新用户注册 + zh-Hant: 停用新使用者註冊 + ja: 新規登録を無効化 + ko: 신규 등록 비활성화 + ru: Отключить регистрацию + ms: Nyahdayakan pendaftaran + pt-br: Desativar cadastro + required: true + type: select + values: + - label: "false" + value: "false" + - label: "true" + value: "true" + - default: kaneo + edit: true + envKey: POSTGRES_DB + labelEn: Database Name + labelZh: 数据库名称 + label: + en: Database Name + zh: 数据库名称 + zh-Hant: 資料庫名稱 + ja: データベース名 + ko: 데이터베이스 이름 + ru: Имя базы данных + ms: Nama pangkalan data + pt-br: Nome do banco de dados + random: true + required: true + rule: paramCommon + type: text + - default: kaneo + edit: true + envKey: POSTGRES_USER + labelEn: Database User + labelZh: 数据库用户 + label: + en: Database User + zh: 数据库用户 + zh-Hant: 資料庫使用者 + ja: データベースユーザー + ko: 데이터베이스 사용자 + ru: Пользователь базы данных + ms: Pengguna pangkalan data + pt-br: Usuario do banco de dados + random: true + required: true + rule: paramCommon + type: text + - default: generate + edit: true + envKey: POSTGRES_PASSWORD + labelEn: Database Password + labelZh: 数据库密码 + label: + en: Database Password + zh: 数据库密码 + zh-Hant: 資料庫密碼 + ja: データベースパスワード + ko: 데이터베이스 비밀번호 + ru: Пароль базы данных + ms: Kata laluan pangkalan data + pt-br: Senha do banco de dados + required: true + type: password + - default: generate + edit: true + envKey: AUTH_SECRET + labelEn: Authentication Secret + labelZh: 认证密钥 + label: + en: Authentication Secret + zh: 认证密钥 + zh-Hant: 驗證密鑰 + ja: 認証シークレット + ko: 인증 비밀 키 + ru: Секрет аутентификации + ms: Rahsia pengesahan + pt-br: Segredo de autenticacao + required: true + type: password + - default: ./data + edit: true + envKey: APP_DATA_DIR + labelEn: Data Directory + labelZh: 数据目录 + label: + en: Data Directory + zh: 数据目录 + zh-Hant: 資料目錄 + ja: データディレクトリ + ko: 데이터 디렉터리 + ru: Каталог данных + ms: Direktori data + pt-br: Diretorio de dados + required: true + type: text diff --git a/apps/kaneo/latest/docker-compose.yml b/apps/kaneo/latest/docker-compose.yml new file mode 100644 index 000000000..08ba7662c --- /dev/null +++ b/apps/kaneo/latest/docker-compose.yml @@ -0,0 +1,73 @@ +services: + kaneo-postgres: + image: postgres:16-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777 + container_name: ${CONTAINER_NAME}-postgres + restart: unless-stopped + user: "70:70" + networks: + - kaneo-internal + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - "${APP_DATA_DIR}/postgres:/var/lib/postgresql/data" + read_only: true + tmpfs: + - /tmp:mode=1777,size=64m + - /var/run/postgresql:uid=70,gid=70,mode=0755,size=16m + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + healthcheck: + test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""] + interval: 10s + timeout: 5s + retries: 10 + start_period: 20s + labels: + createdBy: "Apps" + + kaneo: + image: ghcr.io/usekaneo/kaneo:latest@sha256:d83479f41d437d5c35656f811f345a50357588422fc8d504cfde002756da688e + container_name: ${CONTAINER_NAME} + restart: unless-stopped + user: "1001:1001" + depends_on: + kaneo-postgres: + condition: service_healthy + networks: + - kaneo-internal + - 1panel-network + ports: + - "${PANEL_APP_BIND_ADDRESS}:${PANEL_APP_PORT_HTTP}:5173" + environment: + KANEO_CLIENT_URL: ${KANEO_CLIENT_URL} + POSTGRES_HOST: kaneo-postgres + POSTGRES_PORT: "5432" + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + AUTH_SECRET: ${AUTH_SECRET} + DISABLE_REGISTRATION: ${DISABLE_REGISTRATION} + tmpfs: + - /tmp:mode=1777,size=64m + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:5173/api/health"] + interval: 15s + timeout: 5s + retries: 8 + start_period: 60s + labels: + createdBy: "Apps" + +networks: + kaneo-internal: + internal: true + 1panel-network: + external: true diff --git a/apps/kaneo/latest/scripts/init.sh b/apps/kaneo/latest/scripts/init.sh new file mode 100755 index 000000000..65b615bc8 --- /dev/null +++ b/apps/kaneo/latest/scripts/init.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}" + +fail() { + printf '%s\n' "$1" >&2 + exit 1 +} + +read_env_value() { + local key="$1" + local value + value="$(sed -n "s/^${key}=//p" "$ENV_FILE" | tail -n 1)" + case "$value" in + \"*\") value="${value#\"}"; value="${value%\"}" ;; + \'*\') value="${value#\'}"; value="${value%\'}" ;; + esac + printf '%s\n' "$value" +} + +set_env_value() { + local key="$1" + local value="$2" + local temp_file + temp_file="$(mktemp "${ROOT_DIR}/.kaneo-env.tmp.XXXXXX")" + awk -v key="$key" -v value="$value" ' + BEGIN { written = 0 } + $0 ~ "^" key "=" { + if (!written) { + print key "=" value + written = 1 + } + next + } + { print } + END { if (!written) print key "=" value } + ' "$ENV_FILE" > "$temp_file" + chmod 600 "$temp_file" + mv -f -- "$temp_file" "$ENV_FILE" +} + +validate_ipv4() { + local key="$1" + local value="$2" + local octet + local -a octets + [[ "$value" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || fail "$key must be an IPv4 address" + IFS=. read -r -a octets <<< "$value" + for octet in "${octets[@]}"; do + ((10#$octet <= 255)) || fail "$key contains an invalid IPv4 octet" + done +} + +validate_port() { + local key="$1" + local value="$2" + [[ "$value" =~ ^[0-9]+$ ]] || fail "$key must be an integer" + ((10#$value >= 1 && 10#$value <= 65535)) || fail "$key must be between 1 and 65535" +} + +generate_alphanumeric() { + local length="$1" + local material + material="$(openssl rand -base64 96 | tr -dc A-Za-z0-9)" + [[ ${#material} -ge $length ]] || fail "unable to generate sufficient random material" + printf '%s\n' "${material:0:length}" +} + +[[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found" +[[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link" +[[ "$(id -u)" -eq 0 ]] || fail "Kaneo init must run as root" +command -v openssl >/dev/null 2>&1 || fail "openssl is required to generate credentials" + +validate_ipv4 PANEL_APP_BIND_ADDRESS "$(read_env_value PANEL_APP_BIND_ADDRESS)" +validate_port PANEL_APP_PORT_HTTP "$(read_env_value PANEL_APP_PORT_HTTP)" + +client_url="$(read_env_value KANEO_CLIENT_URL)" +[[ "$client_url" =~ ^https?://([A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?|localhost)(:([0-9]{1,5}))?$ ]] || + fail "KANEO_CLIENT_URL must be an HTTP(S) origin without a path" +[[ "$client_url" != *..* ]] || fail "KANEO_CLIENT_URL contains an invalid hostname" +if [[ -n "${BASH_REMATCH[4]:-}" ]]; then + validate_port KANEO_CLIENT_URL_PORT "${BASH_REMATCH[4]}" +fi + +disable_registration="$(read_env_value DISABLE_REGISTRATION)" +[[ "$disable_registration" == true || "$disable_registration" == false ]] || + fail "DISABLE_REGISTRATION must be true or false" + +postgres_db="$(read_env_value POSTGRES_DB)" +postgres_user="$(read_env_value POSTGRES_USER)" +[[ "$postgres_db" =~ ^[A-Za-z_][A-Za-z0-9_]{0,62}$ ]] || fail "POSTGRES_DB is invalid" +[[ "$postgres_user" =~ ^[A-Za-z_][A-Za-z0-9_]{0,62}$ ]] || fail "POSTGRES_USER is invalid" + +postgres_password="$(read_env_value POSTGRES_PASSWORD)" +if [[ -z "$postgres_password" || "$postgres_password" == generate ]]; then + postgres_password="$(generate_alphanumeric 48)" +fi +[[ "$postgres_password" =~ ^[A-Za-z0-9]{32,128}$ ]] || + fail "POSTGRES_PASSWORD must contain 32 to 128 alphanumeric characters" + +auth_secret="$(read_env_value AUTH_SECRET)" +if [[ -z "$auth_secret" || "$auth_secret" == generate ]]; then + auth_secret="$(openssl rand -hex 32)" +fi +[[ "$auth_secret" =~ ^[A-Fa-f0-9]{64,128}$ ]] || + fail "AUTH_SECRET must contain 64 to 128 hexadecimal characters" + +data_raw="$(read_env_value APP_DATA_DIR)" +[[ -n "$data_raw" && "$data_raw" != /* ]] || fail "APP_DATA_DIR must be a non-empty relative path" +case "$data_raw" in + *$'\n'* | *$'\r'* | *\\* | *:* | *'$'* | *'#'* | *'"'* | *"'"*) + fail "APP_DATA_DIR contains unsupported characters" + ;; +esac + +relative_data="${data_raw#./}" +[[ -n "$relative_data" ]] || fail "APP_DATA_DIR must not resolve to the version root" +current="$ROOT_DIR" +IFS=/ read -r -a components <<< "$relative_data" +for component in "${components[@]}"; do + [[ -n "$component" && "$component" != . && "$component" != .. ]] || fail "APP_DATA_DIR contains traversal" + current="$current/$component" + [[ ! -L "$current" ]] || fail "APP_DATA_DIR must not contain symbolic-link components" +done + +data_dir="$(realpath -m -- "$ROOT_DIR/$relative_data")" +case "$data_dir" in + "$ROOT_DIR"/*) ;; + *) fail "APP_DATA_DIR must stay inside the application version directory" ;; +esac + +install -d -m 0700 -- "$data_dir/postgres" +resolved_data="$(realpath -e -- "$data_dir")" +case "$resolved_data" in + "$ROOT_DIR"/*) ;; + *) fail "APP_DATA_DIR resolves outside the application version directory" ;; +esac +chown -R 70:70 -- "$resolved_data/postgres" +chmod 0700 -- "$resolved_data/postgres" + +set_env_value POSTGRES_PASSWORD "$postgres_password" +set_env_value AUTH_SECRET "$auth_secret" +chmod 600 "$ENV_FILE" diff --git a/apps/kaneo/latest/scripts/uninstall.sh b/apps/kaneo/latest/scripts/uninstall.sh new file mode 100755 index 000000000..5c199242b --- /dev/null +++ b/apps/kaneo/latest/scripts/uninstall.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +if command -v docker-compose >/dev/null 2>&1; then + docker-compose down --remove-orphans +else + docker compose down --remove-orphans +fi diff --git a/apps/kaneo/latest/scripts/upgrade.sh b/apps/kaneo/latest/scripts/upgrade.sh new file mode 100755 index 000000000..5ec28d784 --- /dev/null +++ b/apps/kaneo/latest/scripts/upgrade.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +exec "$SCRIPT_DIR/init.sh" diff --git a/apps/kaneo/logo-LICENSE.txt b/apps/kaneo/logo-LICENSE.txt new file mode 100644 index 000000000..4e2666ad5 --- /dev/null +++ b/apps/kaneo/logo-LICENSE.txt @@ -0,0 +1,4 @@ +Source: https://github.com/usekaneo/kaneo/blob/v2.9.9/apps/site/public/logo-512.png +License: MIT +Copyright: Copyright (c) 2024 Andrej Acevski +The source image is used without changing its design. diff --git a/apps/kaneo/logo.png b/apps/kaneo/logo.png new file mode 100644 index 000000000..94b1be8d0 Binary files /dev/null and b/apps/kaneo/logo.png differ