diff --git a/.env b/.env new file mode 100644 index 0000000..0621931 --- /dev/null +++ b/.env @@ -0,0 +1,42 @@ +LOCAL_TIMEZONE="America/Argentina/Buenos_Aires" +STORAGE_VOLUME="/storage" +APPS_VOLUME="/containers" +GID="${UID}" +PUBLIC_IP="181.14.212.99" +LOCAL_IP="192.168.1.239" +OLLAMA_SERVER="192.168.1.135:11434" +DOMAIN_NAME="sudestec.ar" +PROXY_HTTP_PORT="7700" +PROXY_HTTPS_PORT="7701" +PROXY_PORT="7702" +NEXTCLOUD_PORT="7710" +ONLYOFFICE_PORT="7711" +POCKETBASE_PORT="7720" +GITEA_PORT="7730" +GITEA_PORT2="7731" +FLATNOTE_PORT="7740" +FLATNOTE_NAME="flatnote" +SUPABASE_DATABASE_PASSWORD="facU8990" +SUPABASE_DATABASE="supabase-db" +NEXTCLOUD_TRUSTED_DOMAINS="${LOCAL_IP} ${DOMAIN_NAME}" +NEXTCLOUD_REDIS_ENABLED="" +NEXTCLOUD_VERSION="stable" +OPENWEBUI_PORT="" +VIRTMANAGER_PORT="" +WIREGUARD_PORT="" +GITEA_NAME="git-sudeste" +WIREGUARD_PEERS="10" +WIREGUARD_SUBNET="7.7.7.0" +ADMIN_EMAIL="facu.vdp@gmail.com" +ADMIN_USERNAME="sudeste" +ADMIN_PASSWORD="facU8990!$" +POCKETBASE_VERSION="latest" +POCKETBASE_NAME="pocketbase" +POCKETBASE_TRUSTED_DOMAINS="127.0.0.1,localhost,server-sudestec,192.168.1.239,own.sudestec.ar" +SPEEDTEST_SCHEDULE="*/30 * * * *" +SPEEDTEST_SERVERS="13449,54503" +PHP_MEMORY_LIMIT="1024M" +PHP_UPLOAD_LIMIT="10240M" +JWT_SECRET="100847038046ae5cb373204d405da78c0817285b17e3212a8a2970bf4605cfc2" +ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY4ODg2OTQ4LCJleHAiOjE3Njg4OTA1NDh9.HlpsCqlfMYmHH8ZO-X-Q099awds6o71CEq0qwpbGUSY" +SERVICE_ROLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3Njg4ODY5NDgsImV4cCI6MjA4NDI0Njk0OH0.MW_D_r9FBh9EC-DR8G1mJedXC8EaZyTZNYkGafDE3fU" diff --git a/.gitignore b/.gitignore index aa603d0..d796abe 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ test-servers/ .bundle vendor +test +secret \ No newline at end of file diff --git a/.gitignore copy b/.gitignore copy new file mode 100644 index 0000000..04401ea --- /dev/null +++ b/.gitignore copy @@ -0,0 +1,2 @@ +test +secret \ No newline at end of file diff --git a/commands/01-start-proxy_server.sh b/commands/01-start-proxy_server.sh new file mode 100755 index 0000000..e28bf2e --- /dev/null +++ b/commands/01-start-proxy_server.sh @@ -0,0 +1,33 @@ +#! /bin/bash +source .env + +proxy_path=${APPS_VOLUME}/proxy +proxy_data=${proxy_path}/data +proxy_letsencrypt=${proxy_path}/letsencrypt + +for p in "${proxy_data}" "${proxy_letsencrypt}"; do + if ! ls ${p} >/dev/null 2>&1; then + echo "Creating ${p} directory..." + mkdir -p ${p} + else + echo "directory exists ${p}" + fi +done + +podman run -d --replace \ + --name proxy_server \ + --hostname proxy_server \ + --network container-bridge \ + --tz=local \ + -p ${PROXY_PORT}:81 \ + -p ${PROXY_HTTP_PORT}:80 \ + -p ${PROXY_HTTPS_PORT}:443 \ + -v ${proxy_data}:/data \ + -v ${proxy_letsencrypt}:/etc/letsencrypt \ + --health-cmd="/usr/bin/check-health" \ + --health-interval=10s \ + --health-timeout=3s \ + -e DISABLE_IPV6=true \ + -e INITIAL_ADMIN_EMAIL=${ADMIN_EMAIL} \ + -e INITIAL_ADMIN_PASSWORD=${ADMIN_PASSWORD} \ + docker.io/jc21/nginx-proxy-manager:latest diff --git a/commands/02-start-redis.sh b/commands/02-start-redis.sh new file mode 100755 index 0000000..745b070 --- /dev/null +++ b/commands/02-start-redis.sh @@ -0,0 +1,28 @@ +#! /bin/bash +source .env + +redis_path=${APPS_VOLUME}/redis + +for p in "${redis_path}"; do + if ! ls ${p} >/dev/null 2>&1; then + echo "Creating ${p} directory..." + mkdir -p ${p} + else + echo "directory exists ${p}" + fi +done + +podman run -d --replace \ + --name redis \ + --hostname redis \ + --network container-bridge \ + --tz=local \ + -v ${redis_path}:/data \ + --health-cmd="redis-cli ping" \ + --health-interval=10s \ + --health-timeout=5s \ + --health-retries=5 \ + docker.io/redis:6 \ + --databases 1 + +podman generate systemd --new --files --name redis diff --git a/commands/03-start-nextcloud.sh b/commands/03-start-nextcloud.sh new file mode 100755 index 0000000..bd9bbba --- /dev/null +++ b/commands/03-start-nextcloud.sh @@ -0,0 +1,40 @@ +#! /bin/bash +source .env + +# Create container directories +nc_app=${APPS_VOLUME}/nextcloud +nc_data=${STORAGE_VOLUME}/nextcloud + +for p in "${nc_app} ${nc_data}"; do + if ! ls ${p} >/dev/null 2>&1; then + echo "Creating ${p} directory..." + mkdir -p ${p} + else + echo "directory exists ${p}" + fi +done + +podman run -d --replace \ + --name nextcloud_server \ + --hostname nextcloud_server \ + --restart=always \ + --network container-bridge \ + --tz=local \ + -p ${NEXTCLOUD_PORT}:80 \ + -e NEXTCLOUD_ADMIN_USER=${ADMIN_USERNAME} \ + -e NEXTCLOUD_ADMIN_PASSWORD=${ADMIN_PASSWORD} \ + -e NEXTCLOUD_TRUSTED_DOMAINS="cloud.${DOMAIN_NAME} ${LOCAL_IP} ${PUBLIC_IP}" \ + -e TRUSTED_PROXIES=proxy_server \ + -e TRUSTED_PROXIES=${LOCAL_IP}:${HTTPS_PORT} \ + -e OVERWRITECLIURL=https://cloud.${DOMAIN_NAME} \ + -e SQLITE_DATABASE=nextcloud_db \ + -e REDIS_HOST=redis \ + -e PHP_MEMORY_LIMIT=${PHP_MEMORY_LIMIT} \ + -e PHP_UPLOAD_LIMIT=${PHP_UPLOAD_LIMIT} \ + -v ${nc_app}:/var/www/html \ + -v ${nc_data}:/var/www/html/data \ + --health-interval=30s \ + --health-timeout=10s \ + --health-retries=5 \ + --health-cmd="curl -f ${LOCAL_IP}:${NEXTCLOUD_PORT}" \ + docker.io/nextcloud:${NEXTCLOUD_VERSION} diff --git a/commands/04-start-pocketbase.sh b/commands/04-start-pocketbase.sh new file mode 100755 index 0000000..5e17372 --- /dev/null +++ b/commands/04-start-pocketbase.sh @@ -0,0 +1,30 @@ +#! /bin/bash +source .env + +pb_ap=${STORAGE_VOLUME}/${POCKETBASE_NAME} + +for p in "${pb_ap}/data ${pb_ap}/public"; do + if ! ls ${p} >/dev/null 2>&1; then + echo "Creating ${p} directory..." + mkdir -p ${p} + else + echo "directory exists ${p}" + fi +done + +podman run -d --replace \ + --name ${POCKETBASE_NAME} \ + --hostname ${POCKETBASE_NAME} \ + --restart=always \ + --network container-bridge \ + -p ${POCKETBASE_PORT}:${POCKETBASE_PORT} \ + -e PB_PORT=${POCKETBASE_PORT} \ + -e PB_ADMIN_EMAIL=${ADMIN_EMAIL} \ + -e PB_ADMIN_PASSWORD=${ADMIN_PASSWORD} \ + -v ${pb_ap}/data:/pb_data \ + -v ${pb_ap}/public:/public \ + --health-cmd="wget --no-verbose --tries=1 --spider ${LOCAL_IP}:${POCKETBASE_PORT}/api/health" \ + --health-interval=30s \ + --health-timeout=10s \ + --health-retries=5 \ + ghcr.io/muchobien/pocketbase:latest diff --git a/commands/05-start-gitea.sh b/commands/05-start-gitea.sh new file mode 100755 index 0000000..17b4338 --- /dev/null +++ b/commands/05-start-gitea.sh @@ -0,0 +1,31 @@ +#! /bin/bash +source .env + +git_data=${STORAGE_VOLUME}/${GITEA_NAME} + +for p in "${git_data}"; do + if ! ls ${p} >/dev/null 2>&1; then + echo "Creating ${p} directory..." + mkdir -p ${p} + else + echo "directory exists ${p}" + fi +done + +podman run -d --replace \ + --name ${GITEA_NAME} \ + --hostname ${GITEA_NAME} \ + --restart=always \ + --network container-bridge \ + -p ${GITEA_PORT}:3000 \ + -p ${GITEA_PORT2}:2222 \ + -v ${git_data}:/data \ + -v /etc/timezone:/etc/timezone:ro \ + -v /etc/localtime:/etc/localtime:ro \ + -e USER_UID=1000 \ + -e USER_GID=1000 \ + --health-cmd="curl -f ${LOCAL_IP}:${GITEA_PORT}/api/healthz" \ + --health-interval=30s \ + --health-timeout=10s \ + --health-retries=5 \ + docker.gitea.com/gitea:latest diff --git a/commands/06-start-flatnotes.sh b/commands/06-start-flatnotes.sh new file mode 100755 index 0000000..ca68217 --- /dev/null +++ b/commands/06-start-flatnotes.sh @@ -0,0 +1,33 @@ +#! /bin/bash +source .env + +git_data=${STORAGE_VOLUME}/${FLATNOTE_NAME} + +for p in "${git_data}"; do + if ! ls ${p} >/dev/null 2>&1; then + echo "Creating ${p} directory..." + mkdir -p ${p} + else + echo "directory exists ${p}" + fi +done + +podman run -d --replace \ + --name ${FLATNOTE_NAME} \ + --hostname ${FLATNOTE_NAME} \ + --restart=always \ + --network container-bridge \ + -p ${FLATNOTE_PORT}:8080 \ + -v ${git_data}:/data \ + --userns=keep-id:uid=1000,gid=1000 \ + -e PUID=1000 \ + -e PGID=1000 \ + -e FLATNOTES_AUTH_TYPE=password \ + -e FLATNOTES_USERNAME=${ADMIN_USERNAME} \ + -e FLATNOTES_PASSWORD=${ADMIN_PASSWORD} \ + -e FLATNOTES_SECRET_KEY=${ANON_KEY} \ + --health-cmd="curl -f ${LOCAL_IP}:${FLATNOTE_PORT}/health" \ + --health-interval=30s \ + --health-timeout=10s \ + --health-retries=5 \ + docker.io/dullage/flatnotes:latest diff --git a/commands/DELETE.sh b/commands/DELETE.sh new file mode 100755 index 0000000..e0a875a --- /dev/null +++ b/commands/DELETE.sh @@ -0,0 +1,20 @@ +#! /bin/bash +echo "⚠️ WARNING: This will permanently delete data." + +select choice in "Abort" "Continue"; do + case "$choice" in + "Abort") + echo "Aborted." + exit 1 + ;; + "Continue") + break + ;; + esac +done + +podman container stop --all + +podman system prune --all + +sudo rm -rdf /storage/flatnote diff --git a/commands/generate-keys.sh b/commands/generate-keys.sh new file mode 100755 index 0000000..5aa58d3 --- /dev/null +++ b/commands/generate-keys.sh @@ -0,0 +1,55 @@ +#!/bin/sh +set -eu + +# === config === +JWT_EXP_ANON=3600 +JWT_EXP_SERVICE=315360000 # 10 years +JWT_ISSUER="supabase" + +# === helpers === +b64url() { + openssl base64 -A | tr '+/' '-_' | tr -d '=' +} + +jwt_sign() { + header=$1 + payload=$2 + secret=$3 + + header_b64=$(printf '%s' "$header" | b64url) + payload_b64=$(printf '%s' "$payload" | b64url) + + sig=$(printf '%s.%s' "$header_b64" "$payload_b64" | + openssl dgst -binary -sha256 -hmac "$secret" | b64url) + + printf '%s.%s.%s\n' "$header_b64" "$payload_b64" "$sig" +} + +# === generate JWT secret === +JWT_SECRET=$(openssl rand -hex 32) + +NOW=$(date +%s) + +JWT_HEADER='{"alg":"HS256","typ":"JWT"}' + +ANON_PAYLOAD=$( + cat <&2 +else + echo "Secret created..." >&2 + openssl rand -hex 32 >./secret +fi + +# create network +if ! podman network inspect container-bridge >/dev/null 2>&1; then + echo "Creating container-bridge network..." + podman network create container-bridge +else + echo "container-bridge network already exists" +fi + +# Get podman start scripts +shopt -s nullglob +# start_files=( *-start-*.sh ) +containers=() + +# Run every start stcript +for f in *-start-*.sh; do + trim="${f%.sh}" + container="${trim#*start-}" + containers+=("$container") + echo "Running $container" + chmod +x "$f" + ./"$f" +done + +# Generate systemd Units + +# mkdir -p ~/.config/systemd/user +# for f in "${containers[@]}"; do +# echo "Generating systemd Unit for $f" +# podman generate systemd --files --name $f +# if ! [ -e "patch-${f}-service.sh" ]; then +# echo "No patch for container-${f}.service" +# else +# echo "Patching container-${f}.service..." +# +# fi +# mv container-$f.service ~/.config/systemd/user/ +# done + +# TODO +# Add to container-owncloud_server.service +# Requires=container-owncloud_mariadb.service +# Requires=container-owncloud_redis.service +# After=container-owncloud_mariadb.service +# After=container-owncloud_redis.service + +# Add to container-proxy_server.service +# +# +# +# +# diff --git a/commands/kong.yml b/commands/kong.yml new file mode 100644 index 0000000..168634f --- /dev/null +++ b/commands/kong.yml @@ -0,0 +1,283 @@ +_format_version: '2.1' +_transform: true + +### +### Consumers / Users +### +consumers: + - username: DASHBOARD + - username: anon + keyauth_credentials: + - key: $SUPABASE_ANON_KEY + - username: service_role + keyauth_credentials: + - key: $SUPABASE_SERVICE_KEY + +### +### Access Control List +### +acls: + - consumer: anon + group: anon + - consumer: service_role + group: admin + +### +### Dashboard credentials +### +basicauth_credentials: + - consumer: DASHBOARD + username: $DASHBOARD_USERNAME + password: $DASHBOARD_PASSWORD + +### +### API Routes +### +services: + ## Open Auth routes + - name: auth-v1-open + url: http://auth:9999/verify + routes: + - name: auth-v1-open + strip_path: true + paths: + - /auth/v1/verify + plugins: + - name: cors + - name: auth-v1-open-callback + url: http://auth:9999/callback + routes: + - name: auth-v1-open-callback + strip_path: true + paths: + - /auth/v1/callback + plugins: + - name: cors + - name: auth-v1-open-authorize + url: http://auth:9999/authorize + routes: + - name: auth-v1-open-authorize + strip_path: true + paths: + - /auth/v1/authorize + plugins: + - name: cors + + ## Secure Auth routes + - name: auth-v1 + _comment: 'GoTrue: /auth/v1/* -> http://auth:9999/*' + url: http://auth:9999/ + routes: + - name: auth-v1-all + strip_path: true + paths: + - /auth/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + ## Secure REST routes + - name: rest-v1 + _comment: 'PostgREST: /rest/v1/* -> http://rest:3000/*' + url: http://rest:3000/ + routes: + - name: rest-v1-all + strip_path: true + paths: + - /rest/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: true + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + ## Secure GraphQL routes + - name: graphql-v1 + _comment: 'PostgREST: /graphql/v1/* -> http://rest:3000/rpc/graphql' + url: http://rest:3000/rpc/graphql + routes: + - name: graphql-v1-all + strip_path: true + paths: + - /graphql/v1 + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: true + - name: request-transformer + config: + add: + headers: + - Content-Profile:graphql_public + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + ## Secure Realtime routes + - name: realtime-v1-ws + _comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*' + url: http://realtime-dev.supabase-realtime:4000/socket + protocol: ws + routes: + - name: realtime-v1-ws + strip_path: true + paths: + - /realtime/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + - name: realtime-v1-rest + _comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*' + url: http://realtime-dev.supabase-realtime:4000/api + protocol: http + routes: + - name: realtime-v1-rest + strip_path: true + paths: + - /realtime/v1/api + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + ## Storage routes: the storage server manages its own auth + - name: storage-v1 + _comment: 'Storage: /storage/v1/* -> http://storage:5000/*' + url: http://storage:5000/ + routes: + - name: storage-v1-all + strip_path: true + paths: + - /storage/v1/ + plugins: + - name: cors + + ## Edge Functions routes + - name: functions-v1 + _comment: 'Edge Functions: /functions/v1/* -> http://functions:9000/*' + url: http://functions:9000/ + routes: + - name: functions-v1-all + strip_path: true + paths: + - /functions/v1/ + plugins: + - name: cors + + ## Analytics routes + - name: analytics-v1 + _comment: 'Analytics: /analytics/v1/* -> http://logflare:4000/*' + url: http://analytics:4000/ + routes: + - name: analytics-v1-all + strip_path: true + paths: + - /analytics/v1/ + + ## Secure Database routes + - name: meta + _comment: 'pg-meta: /pg/* -> http://pg-meta:8080/*' + url: http://meta:8080/ + routes: + - name: meta-all + strip_path: true + paths: + - /pg/ + plugins: + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + + ## Block access to /api/mcp + - name: mcp-blocker + _comment: 'Block direct access to /api/mcp' + url: http://studio:3000/api/mcp + routes: + - name: mcp-blocker-route + strip_path: true + paths: + - /api/mcp + plugins: + - name: request-termination + config: + status_code: 403 + message: "Access is forbidden." + + ## MCP endpoint - local access + - name: mcp + _comment: 'MCP: /mcp -> http://studio:3000/api/mcp (local access)' + url: http://studio:3000/api/mcp + routes: + - name: mcp + strip_path: true + paths: + - /mcp + plugins: + # Block access to /mcp by default + - name: request-termination + config: + status_code: 403 + message: "Access is forbidden." + # Enable local access (danger zone!) + # 1. Comment out the 'request-termination' section above + # 2. Uncomment the entire section below, including 'deny' + # 3. Add your local IPs to the 'allow' list + #- name: cors + #- name: ip-restriction + # config: + # allow: + # - 127.0.0.1 + # - ::1 + # deny: [] + + ## Protected Dashboard - catch all remaining routes + - name: dashboard + _comment: 'Studio: /* -> http://studio:3000/*' + url: http://studio:3000/ + routes: + - name: dashboard-all + strip_path: true + paths: + - / + plugins: + - name: cors + - name: basic-auth + config: + hide_credentials: true diff --git a/commands/secret.sh b/commands/secret.sh new file mode 100755 index 0000000..b1b81f5 --- /dev/null +++ b/commands/secret.sh @@ -0,0 +1,10 @@ +#! /bin/bash + +if [[ -f "secret" ]]; then + echo "Found a SECRET! Shhhhh..." >&2 + SECRET=$(&2 + openssl rand -hex 32 >./secret + SECRET=$(