Files
ouranos/docs/casdoor.md

16 KiB

Casdoor SSO Identity Provider

Casdoor provides Single Sign-On (SSO) authentication for Ouranos services. This document covers the design decisions, architecture, and deployment procedures.

Design Philosophy

Security Isolation

Casdoor handles identity and authentication - the most security-sensitive data in any system. For this reason, Casdoor uses a dedicated PostgreSQL instance on Titania rather than sharing the PostgreSQL server on Portia with other applications.

This isolation provides:

  • Data separation: Authentication data is physically separated from application data
  • Access control: The casdoor database user only has access to the casdoor database
  • Blast radius reduction: A compromise of the shared database on Portia doesn't expose identity data
  • Production alignment: Dev/UAT/Prod environments use the same architecture

Native PostgreSQL with Docker Casdoor

The architecture splits cleanly:

┌──────────────────────────────────────────────────────────────┐
│  titania.incus                                               │
│                                                              │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  Native PostgreSQL 17 (systemd)                        │  │
│  │  - SSL enabled for external connections                │  │
│  │  - Local connections without SSL                       │  │
│  │  - Managed like any standard PostgreSQL install        │  │
│  │  - Port 5432                                           │  │
│  └────────────────────────────────────────────────────────┘  │
│           ▲                                                  │
│           │ localhost:5432                                   │
│           │ sslmode=disable                                  │
│           │                                                  │
│  ┌────────┴───────────────────────────────────────────────┐  │
│  │  Casdoor Docker Container (network_mode: host)         │  │
│  │  - Runs as casdoor:casdoor user                        │  │
│  │  - Only has access to its database                     │  │
│  │  - Cannot touch PostgreSQL server config               │  │
│  │  - Port 22081 (via HAProxy)                            │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
└──────────────────────────────────────────────────────────────┘
         │
         │ External: SSL required
         │ sslmode=verify-ca
         ▼
  ┌─────────────┐
  │   PGadmin   │
  │ on Portia   │
  └─────────────┘

Why Not Docker for PostgreSQL?

Docker makes PostgreSQL permission management unnecessarily complex:

  • UID/GID mapping between host and container
  • Volume permission issues
  • SSL certificate ownership problems
  • More difficult backups and maintenance

Native PostgreSQL is:

  • Easier to manage (standard Linux administration)
  • Better integrated with systemd
  • Simpler backup procedures
  • Well-documented and understood

SSL Strategy

PostgreSQL connections follow a split SSL policy:

Connection Source SSL Requirement Rationale
Casdoor (localhost) sslmode=disable Same host, trusted
PGadmin (Portia) sslmode=verify-ca External network, requires encryption
Other external hostssl required Enforced by pg_hba.conf

This is controlled by pg_hba.conf:

# Local connections (Unix socket)
local   all             all                                     peer

# Localhost connections (no SSL required)
host    all             all             127.0.0.1/32            md5

# External connections (SSL required)
hostssl all             all             0.0.0.0/0               md5

System User Pattern

The Casdoor service user is created without hardcoded UID/GID:

- name: Create casdoor user
  ansible.builtin.user:
    name: "{{ casdoor_user }}"
    system: true  # System account, UID assigned by OS

The playbook queries the assigned UID/GID at runtime for Docker container user mapping.

Architecture

Components

Component Location Purpose
PostgreSQL 17 Native on Titania Dedicated identity database
Casdoor Docker on Titania SSO identity provider
HAProxy Titania TLS termination, routing
Alloy Titania Syslog collection

Deployment Order

1. postgresql_ssl/deploy.yml  → Install PostgreSQL, SSL, create casdoor DB
2. casdoor/deploy.yml         → Deploy Casdoor container
3. pgadmin/deploy.yml         → Distribute SSL cert to PGadmin (optional)

Network Ports

Port Service Access
22081 Casdoor HTTP Via HAProxy (network_mode: host)
5432 PostgreSQL SSL for external, plain for localhost
51401 Syslog Local only (Alloy)

Data Persistence

PostgreSQL data (native install):

/var/lib/postgresql/17/main/     # Database files
/etc/postgresql/17/main/         # Configuration
/etc/postgresql/17/main/ssl/     # SSL certificates

Casdoor configuration:

/srv/casdoor/
├── conf/
│   └── app.conf                 # Casdoor configuration
└── docker-compose.yml           # Service definition

Prerequisites

1. Terraform (S3 Buckets)

Casdoor can use S3-compatible storage for avatars and attachments:

cd terraform
terraform apply

2. Ansible Vault Secrets

Add to ansible/inventory/group_vars/all/vault.yml:

# PostgreSQL SSL postgres user password (for Titania's dedicated PostgreSQL)
vault_postgresql_ssl_postgres_password: "secure-postgres-password"

# Casdoor database password
vault_casdoor_db_password: "secure-db-password"

# Casdoor application secrets
vault_casdoor_auth_state: "random-32-char-string"
vault_casdoor_app_client_secret: "generated-client-secret"

# Casdoor initial user passwords (changed after first login)
vault_casdoor_admin_password: "initial-admin-password"
vault_casdoor_hostmaster_password: "initial-hostmaster-password"

# Optional (for RADIUS protocol)
vault_casdoor_radius_secret: "radius-secret"

Generate secrets:

# Database password
openssl rand -base64 24

# Auth state
openssl rand -hex 16

3. Alloy Log Collection

Ensure Alloy is deployed to receive syslog:

ansible-playbook alloy/deploy.yml --limit titania.incus

Deployment

Fresh Installation

cd ansible

# 1. Deploy PostgreSQL with SSL
ansible-playbook postgresql_ssl/deploy.yml

# 2. Deploy Casdoor
ansible-playbook casdoor/deploy.yml

# 3. Update PGadmin with SSL certificate (optional)
ansible-playbook pgadmin/deploy.yml

Verify Deployment

# Check PostgreSQL status
ssh titania.incus "sudo systemctl status postgresql"

# Check Casdoor container
ssh titania.incus "cd /srv/casdoor && docker compose ps"

# Check logs
ssh titania.incus "cd /srv/casdoor && docker compose logs --tail=50"

# Test health endpoint
curl -s http://titania.incus:22081/api/health

Redeployment

To redeploy Casdoor only (database preserved):

ansible-playbook casdoor/remove.yml
ansible-playbook casdoor/deploy.yml

To completely reset (including database):

ansible-playbook casdoor/remove.yml
ssh titania.incus "sudo -u postgres dropdb casdoor"
ssh titania.incus "sudo -u postgres dropuser casdoor"
ansible-playbook postgresql_ssl/deploy.yml
ansible-playbook casdoor/deploy.yml

Configuration Reference

Host Variables

Located in ansible/inventory/host_vars/titania.incus.yml:

# PostgreSQL SSL (dedicated identity database)
postgresql_ssl_postgres_password: "{{ vault_postgresql_ssl_postgres_password }}"
postgresql_ssl_port: 5432
postgresql_ssl_cert_path: /etc/postgresql/17/main/ssl/server.crt

# Casdoor service account (system-assigned UID/GID)
casdoor_user: casdoor
casdoor_group: casdoor
casdoor_directory: /srv/casdoor

# Web
casdoor_port: 22081
casdoor_runmode: dev  # or 'prod'

# Database (connects to localhost PostgreSQL)
casdoor_db_port: 5432
casdoor_db_name: casdoor
casdoor_db_user: casdoor
casdoor_db_password: "{{ vault_casdoor_db_password }}"
casdoor_db_sslmode: disable  # Localhost, no SSL needed

# Logging
casdoor_syslog_port: 51401

SSL Certificate

The self-signed certificate is generated automatically with:

  • Common Name: titania.incus
  • Subject Alt Names: titania.incus, localhost, 127.0.0.1
  • Validity: 10 years (+3650d)
  • Key Size: 4096 bits
  • Location: /etc/postgresql/17/main/ssl/

To regenerate certificates:

ssh titania.incus "sudo rm -rf /etc/postgresql/17/main/ssl/*"
ansible-playbook postgresql_ssl/deploy.yml
ansible-playbook pgadmin/deploy.yml  # Update cert on Portia

PGadmin Connection

To connect from PGadmin on Portia:

  1. Navigate to https://pgadmin.ouranos.helu.ca
  2. Add Server:
    • General tab
      • Name: Titania PostgreSQL (Casdoor)
    • Connection tab
      • Host: titania.incus
      • Port: 5432
      • Database: casdoor
      • Username: casdoor
      • Password: (from vault)
    • SSL tab
      • SSL Mode: Verify-CA
      • Root certificate: /var/lib/pgadmin/certs/titania-postgres-ca.crt

The certificate is automatically distributed by ansible-playbook pgadmin/deploy.yml.

Application Branding & CSS Customization

Casdoor allows extensive customization of login/signup pages through CSS and HTML fields in the Application settings.

Available CSS/HTML Fields

Field Purpose Where Applied
formCss Custom CSS for desktop login forms Login, signup, consent pages
formCssMobile Mobile-specific CSS overrides Mobile views
headerHtml Custom HTML in page header All auth pages (can inject <style> tags)
footerHtml Custom footer HTML Replaces "Powered by Casdoor"
formSideHtml HTML beside the form Side panel content
formBackgroundUrl Background image URL Full-page background
formBackgroundUrlMobile Mobile background image Mobile background
signupHtml Custom HTML for signup page Signup page only
signinHtml Custom HTML for signin page Signin page only

Configuration via init_data.json

Application branding is configured in ansible/casdoor/init_data.json.j2:

{
  "applications": [
    {
      "name": "app-heluca",
      "formCss": "<style>/* Your CSS here */</style>",
      "footerHtml": "<div style=\"text-align:center;\">Powered by Helu.ca</div>",
      "headerHtml": "<style>/* Additional CSS via style tag */</style>",
      "formBackgroundUrl": "https://example.com/bg.jpg"
    }
  ]
}

Example: Custom Theme CSS

The formCss field contains CSS to customize the Ant Design components:

<style>
/* Login panel styling */
.login-panel {
  background-color: #ffffff;
  border-radius: 10px;
  box-shadow: 0 0 30px 20px rgba(255,164,21,0.12);
}

/* Primary button colors */
.ant-btn-primary {
  background-color: #4b96ff !important;
  border-color: #4b96ff !important;
}
.ant-btn-primary:hover {
  background-color: #58c0ff !important;
  border-color: #58c0ff !important;
}

/* Link colors */
a { color: #ffa415; }
a:hover { color: #ffc219; }

/* Input focus states */
.ant-input:focus, .ant-input-focused {
  border-color: #4b96ff !important;
  box-shadow: 0 0 0 2px rgba(75,150,255,0.2) !important;
}

/* Checkbox styling */
.ant-checkbox-checked .ant-checkbox-inner {
  background-color: #4b96ff !important;
  border-color: #4b96ff !important;
}
</style>

Replace the default "Powered by Casdoor" footer:

<div style="text-align:center;padding:10px;color:#666;">
  <a href="https://helu.ca" style="color:#4b96ff;text-decoration:none;">
    Powered by Helu.ca
  </a>
</div>

Organization-Level Theme

Organization settings also affect theming. Configure in the Organization settings:

Setting Purpose
themeData.colorPrimary Primary color (Ant Design)
themeData.borderRadius Border radius for components
themeData.isCompact Compact mode toggle
logo Organization logo
favicon Browser favicon
websiteUrl Organization website

Updating Existing Applications

Changes to init_data.json only apply during initial Casdoor setup. For existing deployments:

  1. Via Admin UI: Applications → Edit → Update CSS/HTML fields
  2. Via API: Use Casdoor's REST API to update application settings
  3. Database reset: Redeploy with initDataNewOnly = false (overwrites existing data)

CSS Class Reference

Common CSS classes for targeting Casdoor UI elements:

Class Element
.login-panel Main login form container
.login-logo-box Logo container
.login-username Username input wrapper
.login-password Password input wrapper
.login-button-box Submit button container
.login-forget-password Forgot password link
.login-signup-link Signup link
.login-languages Language selector
.back-button Back button
.provider-img OAuth provider icons
.signin-methods Sign-in method tabs
.verification-code Verification code input
.login-agreement Terms agreement checkbox

Initial Setup

After deployment, access Casdoor at https://id.ouranos.helu.ca:

  1. Login with default credentials: admin / 123
  2. Change admin password immediately
  3. Create organization for your domain
  4. Create applications for services that need SSO:
    • SearXNG (via OAuth2-Proxy)
    • Grafana
    • Other internal services

OAuth2 Application Setup

For each service:

  1. Applications → Add
  2. Configure OAuth2 settings:
    • Redirect URI: https://service.ouranos.helu.ca/oauth2/callback
    • Grant types: Authorization Code
  3. Note the Client ID and Client Secret for service configuration

Troubleshooting

PostgreSQL Issues

# Check PostgreSQL status
ssh titania.incus "sudo systemctl status postgresql"

# View PostgreSQL logs
ssh titania.incus "sudo journalctl -u postgresql -f"

# Check SSL configuration
ssh titania.incus "sudo -u postgres psql -c 'SHOW ssl;'"
ssh titania.incus "sudo -u postgres psql -c 'SHOW ssl_cert_file;'"

# Test SSL connection externally
openssl s_client -connect titania.incus:5432 -starttls postgres

Casdoor Container Issues

# View container status
ssh titania.incus "cd /srv/casdoor && docker compose ps"

# View logs
ssh titania.incus "cd /srv/casdoor && docker compose logs casdoor"

# Restart
ssh titania.incus "cd /srv/casdoor && docker compose restart"

Database Connection

# Connect as postgres admin
ssh titania.incus "sudo -u postgres psql"

# Connect as casdoor user
ssh titania.incus "sudo -u postgres psql -U casdoor -d casdoor -h localhost"

# List databases
ssh titania.incus "sudo -u postgres psql -c '\l'"

# List users
ssh titania.incus "sudo -u postgres psql -c '\du'"

Health Check

# Casdoor health
curl -s http://titania.incus:22081/api/health | jq

# PostgreSQL accepting connections
ssh titania.incus "pg_isready -h localhost"

Security Considerations

  1. Change default admin password immediately after deployment
  2. Rotate database passwords periodically (update vault, redeploy)
  3. Monitor authentication logs in Grafana (via Alloy/Loki)
  4. SSL certificates have 10-year validity, regenerate if compromised
  5. Backup PostgreSQL data regularly - contains all identity data:
    ssh titania.incus "sudo -u postgres pg_dump casdoor > casdoor_backup.sql"