From 630bd3d3f42480127d0cda73ee3b6a4f4b57552b Mon Sep 17 00:00:00 2001 From: Leopere Date: Thu, 5 Mar 2026 15:20:55 -0500 Subject: [PATCH] bump --- .cursor/rules/protect-subscribe-settings.mdc | 18 ++ README.md | 7 +- authelia-config.tar.gz | Bin 4666 -> 0 bytes docker-compose.production.yml | 2 - docker/ss-atlas/internal/config/config.go | 2 + docker/ss-atlas/internal/handlers/admin.go | 64 ++++++ .../ss-atlas/internal/handlers/dashboard.go | 17 +- docker/ss-atlas/internal/handlers/routes.go | 2 + docker/ss-atlas/internal/handlers/stack.go | 21 +- .../internal/handlers/subscription.go | 55 ++++- docker/ss-atlas/internal/ldap/client.go | 25 +++ docker/ss-atlas/internal/stripe/client.go | 13 ++ docker/ss-atlas/internal/swarm/client.go | 28 +++ .../ss-atlas/templates/pages/dashboard.html | 22 ++ docker/ss-atlas/templates/stack-template.yml | 70 +----- docs/CI_CD_VAULT_SETUP.md | 4 +- docs/OAUTH_SETUP.md | 40 +--- docs/README.md | 6 +- scripts/generate-oauth-secrets.sh | 15 +- stack.production.yml | 209 ------------------ stack.yml | 7 +- 21 files changed, 278 insertions(+), 349 deletions(-) create mode 100644 .cursor/rules/protect-subscribe-settings.mdc delete mode 100644 authelia-config.tar.gz create mode 100644 docker/ss-atlas/internal/handlers/admin.go delete mode 100644 stack.production.yml diff --git a/.cursor/rules/protect-subscribe-settings.mdc b/.cursor/rules/protect-subscribe-settings.mdc new file mode 100644 index 0000000..e41f0e2 --- /dev/null +++ b/.cursor/rules/protect-subscribe-settings.mdc @@ -0,0 +1,18 @@ +--- +description: Never remove or alter subscribe/Stripe configuration +alwaysApply: true +--- + +# Subscribe / Stripe configuration is off-limits + +**Do not under any circumstance:** + +- Remove, comment out, reorder, or rename the `STRIPE_*` or subscribe-related environment variables in `stack.yml` (the `ss-atlas` service `environment:` block). +- Remove or alter the same variables in `.env`. +- Stash, replace, or overwrite `stack.yml` or `.env` in a way that drops or changes the Stripe/subscribe env vars. +- Add logic that clears or overwrites these values at deploy or runtime. + +**Required subscribe-related vars in `stack.yml` for `ss-atlas`:** +`STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `STRIPE_PRICE_ID`, `STRIPE_PRICE_ID_FREE`, `STRIPE_PRICE_ID_YEAR`, `STRIPE_PRICE_ID_MONTH_100`, `STRIPE_PRICE_ID_MONTH_200`, `STRIPE_PAYMENT_LINK`, `FREE_TIER_LIMIT`, `YEAR_TIER_LIMIT`, `MAX_SIGNUPS`. + +**If editing `stack.yml` or deploy flow:** preserve the full `ss-atlas` environment section exactly; only add new vars or change non-Stripe defaults when the user explicitly asks. diff --git a/README.md b/README.md index 7595c45..8210f4d 100644 --- a/README.md +++ b/README.md @@ -78,11 +78,10 @@ For comprehensive CI/CD vault setup and secret management: - `IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY` - OIDC token signing private key (RSA) - `IDENTITY_PROVIDERS_OIDC_JWKS_KEY` - OIDC JWKS validation key (RSA) -#### Client Secrets (4) +#### Client Secrets (3) - `CLIENT_SECRET_HEADSCALE` - Headscale VPN OIDC client secret - `CLIENT_SECRET_HEADADMIN` - Headscale admin panel OIDC client secret - `CLIENT_SECRET_PORTAINER` - Portainer OAuth client secret -- `CLIENT_SECRET_GITEA` - Gitea OAuth client secret ## ๐Ÿงช Testing @@ -137,12 +136,12 @@ Key environment variables for customization: ## ๐Ÿ”— OAuth/OIDC Integration -For advanced OAuth/OIDC setup with services like Portainer and Gitea, see the comprehensive guide: +For advanced OAuth/OIDC setup with services like Portainer, see the comprehensive guide: **๐Ÿ“– [OAuth Setup Guide](docs/OAUTH_SETUP.md)** This includes: -- OAuth client configuration for Portainer and Gitea +- OAuth client configuration for Portainer - Client secret generation and management - CI/CD vault setup instructions - Step-by-step authentication flow setup diff --git a/authelia-config.tar.gz b/authelia-config.tar.gz deleted file mode 100644 index b047e4e6002e850577d3ddbc1b8fb30fc439bd34..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4666 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 uVb?EW3j}O_uH)fPuVHsn9us){2DB%KBS Stripe customer %s", remoteUser, customerID) + u, _ := url.Parse(a.cfg.AppURL + "/dashboard") + q := u.Query() + q.Set("linked", "1") + u.RawQuery = q.Encode() + http.Redirect(w, r, u.String(), http.StatusSeeOther) +} + func (a *App) handlePortal(w http.ResponseWriter, r *http.Request) { customerID := r.FormValue("customer_id") if customerID == "" { - http.Error(w, "customer_id required", http.StatusBadRequest) + redirectWithPortalError(w, r, a.cfg.AppURL+"/dashboard", "No billing account linked. Contact support to manage your subscription.") return } @@ -211,7 +254,7 @@ func (a *App) handlePortal(w http.ResponseWriter, r *http.Request) { func (a *App) handleResubscribe(w http.ResponseWriter, r *http.Request) { customerID := r.FormValue("customer_id") if customerID == "" { - http.Error(w, "customer_id required", http.StatusBadRequest) + redirectWithPortalError(w, r, a.cfg.AppURL+"/dashboard", "No billing account linked. Contact support to resubscribe.") return } @@ -251,3 +294,11 @@ func sanitizeUsername(email string) string { } return clean(local) + clean(domain) } + +func redirectWithPortalError(w http.ResponseWriter, r *http.Request, baseURL, message string) { + u, _ := url.Parse(baseURL) + q := u.Query() + q.Set("portal_error", message) + u.RawQuery = q.Encode() + http.Redirect(w, r, u.String(), http.StatusSeeOther) +} diff --git a/docker/ss-atlas/internal/ldap/client.go b/docker/ss-atlas/internal/ldap/client.go index 1207a43..006d533 100644 --- a/docker/ss-atlas/internal/ldap/client.go +++ b/docker/ss-atlas/internal/ldap/client.go @@ -271,6 +271,31 @@ func (c *Client) CountCustomers() (int, error) { return n, nil } +// DeleteUser removes the user from the customers group (if present) and deletes the LDAP entry. +// Call RemoveStack for the customer stack and prune volumes separately if needed. +func (c *Client) DeleteUser(username string) error { + _ = c.RemoveFromGroup(username, "customers") + conn, err := c.connect() + if err != nil { + return err + } + defer conn.Close() + exists, err := c.userExists(conn, username) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("user %s not found", username) + } + userDN := fmt.Sprintf("uid=%s,ou=people,%s", username, c.cfg.LDAPBaseDN) + delReq := goldap.NewDelRequest(userDN, nil) + if err := conn.Del(delReq); err != nil { + return fmt.Errorf("ldap delete user %s: %w", username, err) + } + log.Printf("deleted ldap user %s", username) + return nil +} + func (c *Client) userExists(conn *goldap.Conn, username string) (bool, error) { searchReq := goldap.NewSearchRequest( fmt.Sprintf("ou=people,%s", c.cfg.LDAPBaseDN), diff --git a/docker/ss-atlas/internal/stripe/client.go b/docker/ss-atlas/internal/stripe/client.go index 6448cf0..c4f0023 100644 --- a/docker/ss-atlas/internal/stripe/client.go +++ b/docker/ss-atlas/internal/stripe/client.go @@ -11,6 +11,7 @@ import ( stripego "github.com/stripe/stripe-go/v84" portalsession "github.com/stripe/stripe-go/v84/billingportal/session" checkoutsession "github.com/stripe/stripe-go/v84/checkout/session" + "github.com/stripe/stripe-go/v84/customer" "github.com/stripe/stripe-go/v84/subscription" ) @@ -102,6 +103,18 @@ func (c *Client) CreateCheckoutForCustomer(customerID string, customerCount int) return checkoutsession.New(params) } +// CreateCustomer creates a Stripe customer with the given email. Returns the customer ID. +func (c *Client) CreateCustomer(email string) (string, error) { + params := &stripego.CustomerParams{ + Email: stripego.String(email), + } + cust, err := customer.New(params) + if err != nil { + return "", err + } + return cust.ID, nil +} + func (c *Client) CreatePortalSession(customerID string) (*stripego.BillingPortalSession, error) { params := &stripego.BillingPortalSessionParams{ Customer: stripego.String(customerID), diff --git a/docker/ss-atlas/internal/swarm/client.go b/docker/ss-atlas/internal/swarm/client.go index 26228ee..f3afa29 100644 --- a/docker/ss-atlas/internal/swarm/client.go +++ b/docker/ss-atlas/internal/swarm/client.go @@ -90,6 +90,34 @@ func (c *Client) RemoveStack(stackName string) error { return nil } +// RemoveStackAndVolumes removes the stack and then any volumes that belonged to it. +func (c *Client) RemoveStackAndVolumes(stackName string) error { + if err := c.RemoveStack(stackName); err != nil { + return err + } + cmd := exec.Command("docker", "volume", "ls", "-q", "--filter", "label=com.docker.stack.namespace="+stackName) + cmd.Env = append(os.Environ(), "DOCKER_HOST="+c.cfg.DockerHost) + output, err := cmd.CombinedOutput() + if err != nil { + log.Printf("warn: list volumes for %s: %s", stackName, strings.TrimSpace(string(output))) + return nil + } + for _, name := range strings.Split(strings.TrimSpace(string(output)), "\n") { + name = strings.TrimSpace(name) + if name == "" { + continue + } + rmCmd := exec.Command("docker", "volume", "rm", name) + rmCmd.Env = append(os.Environ(), "DOCKER_HOST="+c.cfg.DockerHost) + if out, err := rmCmd.CombinedOutput(); err != nil { + log.Printf("warn: remove volume %s: %s", name, strings.TrimSpace(string(out))) + } else { + log.Printf("removed volume %s", name) + } + } + return nil +} + func (c *Client) StackExists(stackName string) (bool, error) { cmd := exec.Command("docker", "stack", "ls", "--format", "{{.Name}}") cmd.Env = append(os.Environ(), "DOCKER_HOST="+c.cfg.DockerHost) diff --git a/docker/ss-atlas/templates/pages/dashboard.html b/docker/ss-atlas/templates/pages/dashboard.html index 3071b68..526aeae 100644 --- a/docker/ss-atlas/templates/pages/dashboard.html +++ b/docker/ss-atlas/templates/pages/dashboard.html @@ -171,6 +171,11 @@

Your Stack

+ {{if .StackError}} +
+ {{.StackError}} +
+ {{end}}
Status {{if .StackRunning}} @@ -222,6 +227,17 @@

Manage

+ {{if .Linked}} +
+ Billing account linked. You can now use Manage Subscription below. +
+ {{end}} + {{if .PortalError}} +
+ {{.PortalError}} +
+ {{end}} + {{if .CustomerID}}
{{if and .SubStatus (eq .SubStatus.Label "Expired")}}
@@ -242,6 +258,12 @@

No refunds for the current billing period. Access continues until the end of your paid month.

+ {{else}} +

Link a Stripe billing account to manage payment or cancel from the portal.

+ + +
+ {{end}}

Account Security

diff --git a/docker/ss-atlas/templates/stack-template.yml b/docker/ss-atlas/templates/stack-template.yml index 348ecc1..ddf9786 100644 --- a/docker/ss-atlas/templates/stack-template.yml +++ b/docker/ss-atlas/templates/stack-template.yml @@ -1,47 +1,22 @@ # ============================================================================= -# CUSTOMER STACK TEMPLATE โ€” Gitea + PostgreSQL +# CUSTOMER STACK TEMPLATE โ€” Uptime Kuma # ============================================================================= -# This is the Docker Swarm stack deployed for each paying customer. -# It defines what product/service they receive when they subscribe. +# Single-service stack for each paying customer. Simple webapp for testing +# routing and auth at https://{{.Domain}}/i/{{.Subdomain}} # -# PRODUCT: Gitea โ€” a self-hosted Git service, backed by PostgreSQL. -# Each customer gets their own isolated instance at a sub-path. +# Traefik: priority 10 ensures /i/{{.Subdomain}} always hits this stack, not +# ss-atlas (priority 1). Strip prefix sends e.g. /i/user/foo -> /foo to the app. # -# Structure: -# web โ€” the application, exposed via Traefik behind Authelia auth -# db โ€” PostgreSQL, internal only (backend network, never exposed) -# -# To sell a different product: replace the `web` image, update the port -# in the Traefik loadbalancer label, and adjust `db` env/image as needed. -# -# Template variables (injected at deploy time by swarm/client.go): -# {{.ID}} - customer's username (unique resource naming) -# {{.Subdomain}} - customer's username (used in path: /i/{subdomain}) -# {{.Domain}} - base domain (e.g. bc.a250.ca) -# {{.TraefikNetwork}} - Traefik overlay network name -# -# Each customer gets their stack at: https://{{.Domain}}/i/{{.Subdomain}} -# Access is restricted to the owning user via Authelia forward-auth. +# Template variables (injected by swarm/client.go): +# {{.ID}}, {{.Subdomain}}, {{.Domain}}, {{.TraefikNetwork}} # ============================================================================= services: web: - image: gitea/gitea:1-rootless - environment: - GITEA__database__DB_TYPE: postgres - GITEA__database__HOST: db:5432 - GITEA__database__NAME: gitea - GITEA__database__USER: gitea - GITEA__database__PASSWD: gitea - GITEA__server__DOMAIN: "{{.Domain}}" - GITEA__server__ROOT_URL: "https://{{.Domain}}/i/{{.Subdomain}}/" - GITEA__server__HTTP_PORT: "3000" - GITEA__security__INSTALL_LOCK: "true" + image: louislam/uptime-kuma:2 volumes: - - gitea_data:/var/lib/gitea - - gitea_config:/etc/gitea + - app_data:/app/data networks: - traefik_net - - backend deploy: replicas: 1 labels: @@ -49,26 +24,11 @@ services: traefik.docker.network: "atlas_{{.TraefikNetwork}}" traefik.http.routers.customer-{{.ID}}-web.rule: "Host(`{{.Domain}}`) && PathPrefix(`/i/{{.Subdomain}}`)" traefik.http.routers.customer-{{.ID}}-web.entrypoints: "websecure" - traefik.http.routers.customer-{{.ID}}-web.priority: "2" + traefik.http.routers.customer-{{.ID}}-web.priority: "10" traefik.http.routers.customer-{{.ID}}-web.tls: "true" traefik.http.routers.customer-{{.ID}}-web.middlewares: "strip-customer-{{.ID}}@swarm,authelia-auth@swarm" traefik.http.middlewares.strip-customer-{{.ID}}.stripprefix.prefixes: "/i/{{.Subdomain}}" - traefik.http.services.customer-{{.ID}}-web.loadbalancer.server.port: "3000" - restart_policy: - condition: on-failure - - db: - image: postgres:16-alpine - environment: - POSTGRES_DB: gitea - POSTGRES_USER: gitea - POSTGRES_PASSWORD: gitea - volumes: - - db_data:/var/lib/postgresql/data - networks: - - backend - deploy: - replicas: 1 + traefik.http.services.customer-{{.ID}}-web.loadbalancer.server.port: "3001" restart_policy: condition: on-failure @@ -76,13 +36,7 @@ networks: traefik_net: external: true name: "atlas_{{.TraefikNetwork}}" - backend: - driver: overlay volumes: - gitea_data: - driver: local - gitea_config: - driver: local - db_data: + app_data: driver: local diff --git a/docs/CI_CD_VAULT_SETUP.md b/docs/CI_CD_VAULT_SETUP.md index e1a84e4..a49336e 100644 --- a/docs/CI_CD_VAULT_SETUP.md +++ b/docs/CI_CD_VAULT_SETUP.md @@ -28,7 +28,6 @@ Your Woodpecker CI vault must contain **12 total secrets** for proper Authelia d | `CLIENT_SECRET_HEADSCALE` | Headscale VPN OIDC client | `./generate-secrets.sh` | | `CLIENT_SECRET_HEADADMIN` | Headscale admin OIDC client | `./generate-secrets.sh` | | `CLIENT_SECRET_PORTAINER` | Portainer OAuth client | `./scripts/generate-oauth-secrets.sh` | -| `CLIENT_SECRET_GITEA` | Gitea OAuth client | `./scripts/generate-oauth-secrets.sh` | ## ๐Ÿš€ Setup Process @@ -64,7 +63,6 @@ export WOODPECKER_TOKEN=your-api-token # Update all secrets (example commands) woodpecker secret update --repository your-repo --name CLIENT_SECRET_PORTAINER --value "$(cat secrets/clients/portainer-secret.txt)" -woodpecker secret update --repository your-repo --name CLIENT_SECRET_GITEA --value "$(cat secrets/clients/gitea-secret.txt)" ``` ## ๐Ÿ”„ Secret Rotation @@ -85,7 +83,7 @@ woodpecker secret update --repository your-repo --name CLIENT_SECRET_GITEA --val # Regenerate OAuth client secrets only ./scripts/generate-oauth-secrets.sh -# Update CLIENT_SECRET_PORTAINER and CLIENT_SECRET_GITEA in vault +# Update CLIENT_SECRET_PORTAINER in vault # Deploy when convenient ``` diff --git a/docs/OAUTH_SETUP.md b/docs/OAUTH_SETUP.md index 77ab191..f1237d9 100644 --- a/docs/OAUTH_SETUP.md +++ b/docs/OAUTH_SETUP.md @@ -1,6 +1,6 @@ # OAuth/OIDC Client Setup Guide -This guide covers setting up OAuth/OIDC authentication for services like Portainer and Gitea using Authelia as the identity provider. +This guide covers setting up OAuth/OIDC authentication for services like Portainer using Authelia as the identity provider. ## ๐Ÿ”ง Overview @@ -27,10 +27,6 @@ Add these to your Woodpecker CI vault: - **Variable**: `CLIENT_SECRET_PORTAINER` - **Value**: Generated from `secrets/clients/portainer-secret.txt` -#### Gitea OAuth -- **Variable**: `CLIENT_SECRET_GITEA` -- **Value**: Generated from `secrets/clients/gitea-secret.txt` - ## ๐Ÿ“ฑ Client Configurations ### Portainer OAuth Setup @@ -75,39 +71,6 @@ Once OAuth is working, remove middleware protection: # traefik.http.routers.portainer.middlewares: authelia_authelia ``` -### Gitea OAuth Setup - -#### 1. Authelia Configuration -Already configured in `docker/authelia/config/configuration.oidc.clients.yml`: - -```yaml -- client_id: gitea - client_name: Gitea - client_secret: {{ secret "/run/secrets/CLIENT_SECRET_GITEA" }} - public: false - authorization_policy: one_factor - consent_mode: implicit - scopes: - - openid - - email - - profile - - groups - redirect_uris: - - https://git.{{ env "TRAEFIK_DOMAIN" }}/user/oauth2/authelia/callback - userinfo_signed_response_alg: none -``` - -#### 2. Gitea OAuth Settings -Configure in Gitea โ†’ Site Administration โ†’ Authentication Sources: - -- **Authentication Type**: OAuth2 -- **Authentication Name**: `Authelia` -- **OAuth2 Provider**: OpenID Connect -- **Client ID**: `gitea` -- **Client Secret**: `` -- **OpenID Connect Auto Discovery URL**: `https://login.a250.ca/.well-known/openid_configuration` -- **Icon URL**: `https://login.a250.ca/static/media/logo.png` (optional) - ## ๐Ÿ”„ Deployment Process ### 1. Generate Secrets @@ -118,7 +81,6 @@ Configure in Gitea โ†’ Site Administration โ†’ Authentication Sources: ### 2. Update CI/CD Vault Add the generated secrets to your Woodpecker CI vault: - `CLIENT_SECRET_PORTAINER` -- `CLIENT_SECRET_GITEA` ### 3. Deploy Authelia Push changes to trigger CI/CD deployment with new OAuth clients. diff --git a/docs/README.md b/docs/README.md index 1e73c81..6bf70f6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,7 +5,7 @@ This directory contains comprehensive guides for Authelia deployment and configu ## ๐Ÿ“š Available Guides ### ๐Ÿ”ง Setup & Configuration -- **[OAuth/OIDC Setup Guide](OAUTH_SETUP.md)** - Complete OAuth integration for Portainer, Gitea, and other services +- **[OAuth/OIDC Setup Guide](OAUTH_SETUP.md)** - Complete OAuth integration for Portainer and other services - **[CI/CD Vault Setup](CI_CD_VAULT_SETUP.md)** - Secret management and Woodpecker CI vault configuration ### ๐Ÿš€ Getting Started @@ -18,7 +18,7 @@ This directory contains comprehensive guides for Authelia deployment and configu 2. **OAuth Integration** - Generate OAuth client secrets with `./scripts/generate-oauth-secrets.sh` - Follow [OAuth Setup Guide](OAUTH_SETUP.md) for service configuration - - Configure individual services (Portainer, Gitea) with OAuth + - Configure individual services (e.g. Portainer) with OAuth 3. **Production Deployment** - Commit changes to trigger CI/CD pipeline @@ -55,7 +55,7 @@ docker compose -f docker-compose.dev.yml up -d ### Required Secrets (12 Total) - **Core Secrets (5)**: LDAP, JWT, encryption, session, SMTP - **OIDC Secrets (3)**: HMAC, private key, JWKS key -- **Client Secrets (4)**: Headscale (2), Portainer, Gitea +- **Client Secrets (3)**: Headscale (2), Portainer ## ๐Ÿ” Troubleshooting diff --git a/scripts/generate-oauth-secrets.sh b/scripts/generate-oauth-secrets.sh index d05bd0d..06c76f5 100755 --- a/scripts/generate-oauth-secrets.sh +++ b/scripts/generate-oauth-secrets.sh @@ -106,11 +106,6 @@ Add these secrets to your Woodpecker CI vault: - **Secret File**: `secrets/clients/portainer-secret.txt` - **Value**: (copy content from the file above) -### Gitea OAuth -- **Variable Name**: `CLIENT_SECRET_GITEA` -- **Secret File**: `secrets/clients/gitea-secret.txt` -- **Value**: (copy content from the file above) - ## Important Notes 1. **Never commit these files** - they are automatically gitignored @@ -124,9 +119,6 @@ If using Woodpecker CLI: ```bash # Update Portainer secret woodpecker secret update --repository your-repo --name CLIENT_SECRET_PORTAINER --value "$(cat secrets/clients/portainer-secret.txt)" - -# Update Gitea secret -woodpecker secret update --repository your-repo --name CLIENT_SECRET_GITEA --value "$(cat secrets/clients/gitea-secret.txt)" ``` ## Verification @@ -149,17 +141,15 @@ print_summary() { echo "${YELLOW}๐Ÿ“ Generated Files:${NC}" echo " โ€ข secrets/oauth-secrets.env" echo " โ€ข secrets/clients/portainer-secret.txt" - echo " โ€ข secrets/clients/gitea-secret.txt" echo " โ€ข secrets/VAULT_SECRETS.md" echo echo "${YELLOW}๐Ÿ”‘ Required CI/CD Vault Updates:${NC}" echo " โ€ข CLIENT_SECRET_PORTAINER" - echo " โ€ข CLIENT_SECRET_GITEA" echo echo "${RED}โš ๏ธ NEXT STEPS:${NC}" echo " 1. Update your CI/CD vault with new secrets" echo " 2. Deploy Authelia to use new client configurations" - echo " 3. Configure OAuth in Portainer and Gitea admin panels" + echo " 3. Configure OAuth in Portainer admin panel" echo " 4. Test authentication flows" echo echo "${BLUE}๐Ÿ“– Full setup guide: docs/OAUTH_SETUP.md${NC}" @@ -195,8 +185,7 @@ main() { # Generate client secrets generate_client_secret "portainer" "portainer-secret.txt" - generate_client_secret "gitea" "gitea-secret.txt" - + create_vault_instructions print_summary } diff --git a/stack.production.yml b/stack.production.yml deleted file mode 100644 index 4860062..0000000 --- a/stack.production.yml +++ /dev/null @@ -1,209 +0,0 @@ -x-authelia-env: &authelia-env - X_AUTHELIA_EMAIL: authelia@a250.ca - X_AUTHELIA_SITE_NAME: ATLAS - X_AUTHELIA_CONFIG_FILTERS: template - X_AUTHELIA_LDAP_DOMAIN: dc=a250,dc=ca - TRAEFIK_DOMAIN: bc.a250.ca - -secrets: - AUTHENTICATION_BACKEND_LDAP_PASSWORD: - external: true - IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET: - external: true - # TEMPORARILY DISABLED - OIDC provider disabled - # IDENTITY_PROVIDERS_OIDC_HMAC_SECRET: - # external: true - # IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY: - # external: true - # IDENTITY_PROVIDERS_OIDC_JWKS_KEY: - # external: true - NOTIFIER_SMTP_PASSWORD: - external: true - SESSION_SECRET: - external: true - STORAGE_ENCRYPTION_KEY: - external: true - # TEMPORARILY DISABLED - OAuth clients disabled - # CLIENT_SECRET_HEADSCALE: - # external: true - # CLIENT_SECRET_HEADADMIN: - # external: true - # CLIENT_SECRET_PORTAINER: - # external: true - # TEMPORARILY DISABLED - Gitea OAuth (not ready yet) - # CLIENT_SECRET_GITEA: - # external: true - -networks: - default: - driver: overlay - traefik: - external: true - ad: - external: true - -volumes: - authelia_config: - driver: local - authelia_assets: - driver: local - authelia_redis_data: - driver: local - authelia_mariadb_data: - driver: local - lldap_data: - driver: local - -services: - authelia: - image: git.nixc.us/a250/authelia:production-authelia - command: - - authelia - - --config=/config/configuration.server.yml - - --config=/config/configuration.ldap.yml - - --config=/config/configuration.acl.yml - - --config=/config/configuration.notifier.yml - secrets: - - AUTHENTICATION_BACKEND_LDAP_PASSWORD - - IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET - # - IDENTITY_PROVIDERS_OIDC_HMAC_SECRET - # - IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY - # - IDENTITY_PROVIDERS_OIDC_JWKS_KEY - - NOTIFIER_SMTP_PASSWORD - - SESSION_SECRET - - STORAGE_ENCRYPTION_KEY - # - CLIENT_SECRET_HEADSCALE - # - CLIENT_SECRET_HEADADMIN - # - CLIENT_SECRET_PORTAINER - environment: *authelia-env - dns: - - 1.1.1.1 # Cloudflare - - 9.9.9.9 # Quad9 - volumes: - # - authelia_config:/config:rw - - authelia_assets:/config/assets:rw - networks: - - traefik - - default - - ad - deploy: - update_config: - order: start-first - failure_action: rollback - parallelism: 1 - restart_policy: - condition: on-failure - replicas: 1 - labels: - us.a250.autodeploy: "true" - homepage.group: Infrastructure - homepage.name: Authelia - homepage.href: https://login.bc.a250.ca - homepage.description: ATLAS - traefik.enable: "true" - traefik.docker.network: traefik - traefik.http.routers.authelia_authelia.rule: Host(`login.bc.a250.ca`) - traefik.http.routers.authelia_authelia.entrypoints: web - traefik.http.routers.authelia_authelia.service: authelia_authelia - traefik.http.services.authelia_authelia.loadbalancer.server.port: 9091 - traefik.http.middlewares.authelia_authelia.forwardauth.address: http://authelia_authelia:9091/api/verify?rd=https://login.bc.a250.ca/ - traefik.http.middlewares.authelia_authelia.forwardauth.trustForwardHeader: "true" - traefik.http.middlewares.authelia_authelia.forwardauth.authResponseHeaders: Remote-User,Remote-Groups,Remote-Name,Remote-Email - traefik.http.middlewares.authelia-basic.forwardauth.address: http://authelia_authelia:9091/api/verify?auth=basic - traefik.http.middlewares.authelia-basic.forwardauth.trustForwardHeader: "true" - traefik.http.middlewares.authelia-basic.forwardauth.authResponseHeaders: Remote-User,Remote-Groups,Remote-Name,Remote-Email - # healthcheck: - # test: ["CMD", "nc", "-z", "localhost", "9091"] - # start_period: 30s - # interval: 30s - # timeout: 10s - # retries: 3 - logging: - driver: json-file - options: - max-size: 10m - max-file: "3" - - redis: - image: git.nixc.us/a250/authelia:production-redis - command: redis-server --appendonly yes - volumes: - - authelia_redis_data:/data:rw - networks: - - default - deploy: - update_config: - order: start-first - failure_action: rollback - parallelism: 1 - restart_policy: - condition: on-failure - replicas: 1 - labels: - us.a250.autodeploy: "true" - traefik.enable: "false" - # healthcheck: - # test: ["CMD", "redis-cli", "ping"] - # start_period: 10s - # interval: 30s - # timeout: 5s - # retries: 3 - logging: - driver: json-file - options: - max-size: 10m - max-file: "3" - - lldap: - image: nitnelave/lldap:latest - volumes: - - lldap_data:/data - environment: - LLDAP_JWT_SECRET: I2sNvGvhzZlTJWPfNL9MBPFGhyG/gWU5wHz6wFsIC3I= - LLDAP_LDAP_USER_PASS: /ETAToLiZPWo6QK171abAUqsa3WDpd9IgneZnTA4zU0= - LLDAP_LDAP_BASE_DN: dc=a250,dc=ca - networks: - - default - deploy: - restart_policy: - condition: on-failure - replicas: 1 - logging: - driver: json-file - options: - max-size: 10m - max-file: "3" - - mariadb: - image: git.nixc.us/a250/authelia:production-mariadb - environment: - MYSQL_ROOT_PASSWORD: authelia - MYSQL_DATABASE: authelia - MYSQL_USER: authelia - MYSQL_PASSWORD: authelia - volumes: - - authelia_mariadb_data:/var/lib/mysql:rw - networks: - - default - deploy: - update_config: - order: start-first - failure_action: rollback - parallelism: 1 - restart_policy: - condition: on-failure - replicas: 1 - labels: - us.a250.autodeploy: "true" - traefik.enable: "false" - # healthcheck: - # test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "authelia", "-pauthelia"] - # start_period: 15s - # interval: 30s - # timeout: 10s - # retries: 3 - logging: - driver: json-file - options: - max-size: 10m - max-file: "3" \ No newline at end of file diff --git a/stack.yml b/stack.yml index ba6698a..eaf7a6c 100644 --- a/stack.yml +++ b/stack.yml @@ -78,8 +78,7 @@ services: echo "$${CLIENT_SECRET_HEADSCALE}" > /run/secrets/CLIENT_SECRET_HEADSCALE echo "$${CLIENT_SECRET_HEADADMIN}" > /run/secrets/CLIENT_SECRET_HEADADMIN echo "$${CLIENT_SECRET_PORTAINER}" > /run/secrets/CLIENT_SECRET_PORTAINER - echo "$${CLIENT_SECRET_GITEA}" > /run/secrets/CLIENT_SECRET_GITEA - { echo 'access_control:'; echo ' default_policy: deny'; echo ' rules:'; echo ' - domain: bc.a250.ca'; echo ' policy: bypass'; echo ' resources:'; echo " - '^/login(/.*)?$$'"; echo ' - domain: bc.a250.ca'; echo ' policy: bypass'; echo ' resources:'; echo " - '^/$$'"; echo " - '^/subscribe/?$$'"; echo " - '^/success(/|\\?.*)?$$'"; echo " - '^/webhook/stripe/?$$'"; echo " - '^/resend-reset/?$$'"; echo " - '^/health/?$$'"; echo " - '^/version/?$$'"; echo ' - domain: bc.a250.ca'; echo ' policy: one_factor'; echo ' resources:'; echo " - '^/activate/?$$'"; echo " - '^/dashboard/?$$'"; echo ' - domain: bc.a250.ca'; echo ' subject:'; echo ' - group:customers'; echo ' policy: two_factor'; echo ' resources:'; echo " - '^/portal/?$$'"; echo " - '^/resubscribe/?$$'"; echo " - '^/stack-manage/?$$'"; echo " - '^/i/[a-z0-9-]+(/.*)?$$'"; echo ' - domain: bc.a250.ca'; echo ' policy: bypass'; echo ' resources:'; echo " - '^/admin/lldap(/.*)?$$'"; echo " - '^/whoami(/.*)?$$'"; echo ' - domain: bc.a250.ca'; echo ' policy: deny'; } > /config/configuration.acl.yml + { echo 'access_control:'; echo ' default_policy: deny'; echo ' rules:'; echo ' - domain: bc.a250.ca'; echo ' policy: bypass'; echo ' resources:'; echo " - '^/login(/.*)?$$'"; echo ' - domain: bc.a250.ca'; echo ' policy: bypass'; echo ' resources:'; echo " - '^/$$'"; echo " - '^/subscribe/?$$'"; echo " - '^/success(/|\\?.*)?$$'"; echo " - '^/webhook/stripe/?$$'"; echo " - '^/resend-reset/?$$'"; echo " - '^/health/?$$'"; echo " - '^/version/?$$'"; echo " - '^/admin/delete-user/?$$'"; echo ' - domain: bc.a250.ca'; echo ' policy: one_factor'; echo ' resources:'; echo " - '^/activate/?$$'"; echo " - '^/dashboard/?$$'"; echo ' - domain: bc.a250.ca'; echo ' subject:'; echo ' - group:customers'; echo ' policy: two_factor'; echo ' resources:'; echo " - '^/link-stripe-customer/?$$'"; echo " - '^/portal/?$$'"; echo " - '^/resubscribe/?$$'"; echo " - '^/stack-manage/?$$'"; echo " - '^/i/[a-z0-9-]+(/.*)?$$'"; echo ' - domain: bc.a250.ca'; echo ' policy: bypass'; echo ' resources:'; echo " - '^/admin/lldap(/.*)?$$'"; echo " - '^/whoami(/.*)?$$'"; echo ' - domain: bc.a250.ca'; echo ' policy: deny'; } > /config/configuration.acl.yml exec authelia --config=/config/configuration.server.yml --config=/config/configuration.ldap.yml --config=/config/configuration.acl.yml --config=/config/configuration.notifier.yml --config=/config/configuration.identity.providers.yml --config=/config/configuration.oidc.clients.yml environment: AUTHELIA_SERVER_ADDRESS: tcp://0.0.0.0:9091/login @@ -127,7 +126,6 @@ services: CLIENT_SECRET_HEADSCALE: t4Hvp6DnpA0T+0ePbdx8lPIAujFMrkjEnx5aMQkMFiA= CLIENT_SECRET_HEADADMIN: RAxwkJxwMBSYkaA0r+D5qZdEFIrVEZJbigOPtkCBED8= CLIENT_SECRET_PORTAINER: t4Hvp6DnpA0T+0ePbdx8lPIAujFMrkjEnx5aMQkMFiA= - CLIENT_SECRET_GITEA: t4Hvp6DnpA0T+0ePbdx8lPIAujFMrkjEnx5aMQkMFiA= volumes: - authelia_data:/data networks: @@ -182,6 +180,8 @@ services: - "traefik.http.middlewares.strip-traefik.stripprefix.prefixes=/admin/traefik" - "traefik.http.services.traefik-api.loadbalancer.server.port=8080" + # SUBSCRIBE/STRIPE: Do not remove or reorder the STRIPE_* and tier env vars below. + # They are loaded from .env at deploy time. See .cursor/rules/protect-subscribe-settings.mdc ss-atlas: image: atlas-ss-atlas:latest environment: @@ -212,6 +212,7 @@ services: - ARCHIVE_PATH=/archives - LANDING_TAGLINE=${LANDING_TAGLINE:-Your own workspace, ready in minutes.} - LANDING_FEATURES=${LANDING_FEATURES:-Dedicated environment|Secure single sign-on|Automatic provisioning|Manage subscription anytime} + - ADMIN_SECRET=${ADMIN_SECRET:-} volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - atlas_archives:/archives