diff --git a/.claude/JOURNAL.md b/.claude/JOURNAL.md
index bad024d..b456c8d 100644
--- a/.claude/JOURNAL.md
+++ b/.claude/JOURNAL.md
@@ -162,3 +162,6 @@ This journal tracks substantive work on documents, diagrams, and documentation c
53. **Task - Create stop.sh script**: Added stop.sh to complement start.sh for platform shutdown
**Result**: Created stop.sh mirroring start.sh pattern - resolves script location via readlink/dirname, respects compose_override.yml if present, runs docker compose down --remove-orphans
+
+54. **Task - Enhance traefik-host-based-routing template**: Major improvements to deployment template with CIFS support and certificate installers
+ **Result**: Added optional CIFS mount support via compose_cifs.yml and .env.example (ENABLE_CIFS=1), created install_cert.sh for Linux (multi-distro: Debian/Ubuntu, RHEL/CentOS, Arch, Alpine, macOS) and enhanced install_cert.bat for Windows with folder argument and help flags, fixed compose_override.yml stray quote and added JUPYTERHUB_IDLE_CULLER_ENABLED/JUPYTERHUB_SIGNUP_ENABLED defaults, enhanced generate-certs.sh with generic CN for browser compatibility and verification output, updated start.sh/stop.sh to load .env and conditionally include compose_cifs.yml, updated README with CIFS instructions and certificate installation commands, added .env to .gitignore
diff --git a/extra/traefik-host-based-routing/.env.example b/extra/traefik-host-based-routing/.env.example
new file mode 100644
index 0000000..7badb17
--- /dev/null
+++ b/extra/traefik-host-based-routing/.env.example
@@ -0,0 +1,6 @@
+# Optional environment variables
+# Copy to .env and customize
+
+# Enable CIFS mount for shared storage (0=disabled, 1=enabled)
+# Requires compose_cifs.yml to be configured with NAS credentials
+ENABLE_CIFS=0
diff --git a/extra/traefik-host-based-routing/.gitignore b/extra/traefik-host-based-routing/.gitignore
index 414d185..02c6721 100644
--- a/extra/traefik-host-based-routing/.gitignore
+++ b/extra/traefik-host-based-routing/.gitignore
@@ -15,6 +15,9 @@ Thumbs.db
# Docker
.docker/
+# Environment (may contain credentials)
+.env
+
# TLS certificates (private keys)
certs/*.pem
certs/**/*.pem
diff --git a/extra/traefik-host-based-routing/README.md b/extra/traefik-host-based-routing/README.md
index 971ea2a..d5ddad2 100644
--- a/extra/traefik-host-based-routing/README.md
+++ b/extra/traefik-host-based-routing/README.md
@@ -27,9 +27,13 @@ Template for deploying stellars-jupyterhub-ds with local Traefik reverse proxy a
```
_stellars_jupyterhub_ds/
compose_override.yml # Local Traefik + JupyterHub config
- start.sh # Pull latest + start services
+ compose_cifs.yml # Optional CIFS mount configuration
+ start.sh # Clone/update + start services
stop.sh # Stop services
generate-certs.sh # Certificate generation script
+ install_cert.sh # Linux certificate installer
+ install_cert.bat # Windows certificate installer
+ .env.example # Example environment config
certs/
tls.yml # Traefik TLS configuration
_.yourdomain.example.com/ # Generated wildcard cert
@@ -43,8 +47,19 @@ Template for deploying stellars-jupyterhub-ds with local Traefik reverse proxy a
Edit `compose_override.yml` to customize:
- Domain name (replace `YOURDOMAIN` placeholder)
- Ports (default: 80/443)
+- Environment variables (idle culler, signup)
- Network name
-- Additional services
+
+### Optional CIFS Mount
+
+To enable shared NAS storage for user containers:
+
+1. Edit `compose_cifs.yml` with your NAS credentials
+2. Create `.env` from `.env.example`:
+ ```bash
+ cp .env.example .env
+ ```
+3. Set `ENABLE_CIFS=1` in `.env`
## Access
@@ -52,13 +67,26 @@ After deployment:
- JupyterHub: https://jupyterhub.yourdomain.example.com/
- Traefik: https://traefik.yourdomain.example.com
-Import `certs/_./cert.pem` to browser for trusted HTTPS.
+### Certificate Installation
+
+Import the self-signed certificate to your browser for trusted HTTPS:
+
+**Linux:**
+```bash
+./install_cert.sh certs/_.yourdomain.example.com/
+```
+
+**Windows:**
+```cmd
+install_cert.bat certs\_.yourdomain.example.com\
+```
## Commands
```bash
-./start.sh # Pull latest + start services
-./stop.sh # Stop all services
+./start.sh # Clone repo (if missing) + start services
+./start.sh --refresh # Pull latest upstream + start services
+./stop.sh # Stop all services
```
To view logs:
diff --git a/extra/traefik-host-based-routing/compose_cifs.yml b/extra/traefik-host-based-routing/compose_cifs.yml
new file mode 100644
index 0000000..d5771b1
--- /dev/null
+++ b/extra/traefik-host-based-routing/compose_cifs.yml
@@ -0,0 +1,20 @@
+# =============================================================================
+# CIFS Volume Mount - Optional
+# =============================================================================
+#
+# Enable by setting ENABLE_CIFS=1 in .env
+#
+# Update credentials and mount path below before enabling.
+#
+# =============================================================================
+
+volumes:
+ jupyterhub_shared:
+ driver: local
+ name: jupyterhub_shared
+ driver_opts:
+ type: cifs
+ device: //nas.example.com/shared
+ o: username=YOUR_USERNAME,password=YOUR_PASSWORD,uid=1000,gid=1000,vers=3.0
+
+# EOF
diff --git a/extra/traefik-host-based-routing/compose_override.yml b/extra/traefik-host-based-routing/compose_override.yml
index 8daf9f9..6aa7a36 100644
--- a/extra/traefik-host-based-routing/compose_override.yml
+++ b/extra/traefik-host-based-routing/compose_override.yml
@@ -7,7 +7,7 @@
# ACCESS: https://jupyterhub.YOURDOMAIN/ or https://jupyterhub.localhost/
# TRUST: Import certs/_.YOURDOMAIN/cert.pem to browser
#
-# Replace YOURDOMAIN with your actual domain (e.g., lab.stellars-tech.eu)
+# Replace YOURDOMAIN with your actual domain (e.g., lab.example.com)
#
# =============================================================================
@@ -68,6 +68,8 @@ services:
ports: []
environment:
- JUPYTERHUB_BASE_URL=/
+ - JUPYTERHUB_IDLE_CULLER_ENABLED=1
+ - JUPYTERHUB_SIGNUP_ENABLED=0
networks:
- jupyterhub_network
labels:
@@ -75,7 +77,6 @@ services:
# JupyterHub router (root path)
- "traefik.http.routers.jupyterhub-rtr.rule=Host(`jupyterhub.YOURDOMAIN`) || Host(`jupyterhub.localhost`)"
-"
- "traefik.http.routers.jupyterhub-rtr.entrypoints=websecure"
- "traefik.http.routers.jupyterhub-rtr.tls=true"
- "traefik.http.routers.jupyterhub-rtr.service=jupyterhub-svc"
diff --git a/extra/traefik-host-based-routing/generate-certs.sh b/extra/traefik-host-based-routing/generate-certs.sh
index 6a09338..6544972 100755
--- a/extra/traefik-host-based-routing/generate-certs.sh
+++ b/extra/traefik-host-based-routing/generate-certs.sh
@@ -4,13 +4,16 @@
# =============================================================================
#
# Usage: ./generate-certs.sh
-# Example: ./generate-certs.sh lab.stellars-tech.eu
+# Example: ./generate-certs.sh lab.example.com
#
# Creates:
# certs/_.domain/cert.pem - Certificate (import to browser)
# certs/_.domain/key.pem - Private key
# certs/tls.yml - Traefik TLS configuration
#
+# Note: Uses generic CN to avoid browser CN validation issues across multiple
+# domains. All domains are specified in SAN (Subject Alternative Name) field.
+#
# =============================================================================
set -e
@@ -19,7 +22,7 @@ DOMAIN="${1:-}"
if [ -z "$DOMAIN" ]; then
echo "Usage: $0 "
- echo "Example: $0 lab.stellars-tech.eu"
+ echo "Example: $0 lab.example.com"
exit 1
fi
@@ -32,16 +35,17 @@ echo "Generating self-signed certificate for *.${DOMAIN}"
mkdir -p "$CERT_DIR"
# Generate self-signed certificate
+# Uses generic CN to avoid browser CN validation issues; domains are in SAN
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout "${CERT_DIR}/key.pem" \
-out "${CERT_DIR}/cert.pem" \
- -subj "/CN=*.${DOMAIN}" \
- -addext "subjectAltName=DNS:*.${DOMAIN},DNS:${DOMAIN},DNS:localhost,DNS:*.localhost"
+ -subj "/CN=DEV Certificate" \
+ -addext "subjectAltName=DNS:*.${DOMAIN},DNS:${DOMAIN},DNS:*.app.localhost,DNS:app.localhost,DNS:*.localhost,DNS:localhost"
# Generate Traefik TLS configuration
cat > "$TLS_CONFIG" << EOF
# TLS Configuration for self-signed certificates
-# Wildcard cert: *.${DOMAIN}
+# Wildcard cert: *.${DOMAIN}, *.localhost
# Import cert.pem to browser for trusted HTTPS
tls:
@@ -57,12 +61,15 @@ tls:
EOF
echo ""
-echo "Certificate generated successfully:"
-echo " - ${CERT_DIR}/cert.pem (import to browser)"
-echo " - ${CERT_DIR}/key.pem"
-echo " - ${TLS_CONFIG}"
+echo "Certificate generated:"
+openssl x509 -in "${CERT_DIR}/cert.pem" -noout -subject -dates -ext subjectAltName
+
+echo ""
+echo "Key verified:"
+openssl rsa -in "${CERT_DIR}/key.pem" -check -noout
+
echo ""
echo "Next steps:"
echo " 1. Edit compose_override.yml - replace YOURDOMAIN with ${DOMAIN}"
echo " 2. Import ${CERT_DIR}/cert.pem to your browser"
-echo " 3. Run: make start"
+echo " 3. Run: ./start.sh"
diff --git a/extra/traefik-host-based-routing/install_cert.bat b/extra/traefik-host-based-routing/install_cert.bat
index 25eac29..9edcbcc 100755
--- a/extra/traefik-host-based-routing/install_cert.bat
+++ b/extra/traefik-host-based-routing/install_cert.bat
@@ -1,10 +1,29 @@
@echo off
setlocal enabledelayedexpansion
+REM Check for help flag
+if "%~1"=="-h" goto :show_help
+if "%~1"=="--help" goto :show_help
+if "%~1"=="/?" goto :show_help
+
+REM Optional argument: folder to search for certificates (default: current directory)
+set "CERT_DIR=%~1"
+if "%CERT_DIR%"=="" set "CERT_DIR=."
+
+REM Check if directory exists
+if not exist "%CERT_DIR%\" (
+ echo Error: Directory '%CERT_DIR%' not found.
+ echo Use --help for usage information.
+ pause
+ exit /b 1
+)
+
echo ============================================
echo Certificate Installer - Root Trust Store
echo ============================================
echo.
+echo Scanning directory: %CERT_DIR%
+echo.
echo WARNING: This script installs certificates
echo into your Trusted Root Certification
echo Authorities store.
@@ -32,47 +51,134 @@ if /i not "%proceed%"=="Y" (
)
echo.
-set "found=0"
+echo Scanning for certificate and key files...
+echo.
-for %%F in (*.cer *.crt *.pem *.der) do (
+set "found=0"
+set "certcount=0"
+set "keycount=0"
+
+for %%F in ("%CERT_DIR%\*.cer" "%CERT_DIR%\*.crt" "%CERT_DIR%\*.pem" "%CERT_DIR%\*.der" "%CERT_DIR%\*.key" "%CERT_DIR%\*.p12" "%CERT_DIR%\*.pfx") do (
set "found=1"
echo --------------------------------------------
echo File: %%F
echo --------------------------------------------
- REM Get certificate details using PowerShell
- powershell -Command ^
- "$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2('%%F'); " ^
- "Write-Host 'Subject (CN):' $cert.Subject; " ^
- "Write-Host 'Issuer:' $cert.Issuer; " ^
- "Write-Host 'Valid From:' $cert.NotBefore; " ^
- "Write-Host 'Valid To:' $cert.NotAfter; " ^
- "Write-Host 'Thumbprint:' $cert.Thumbprint; " ^
- "$san = $cert.Extensions | Where-Object { $_.Oid.FriendlyName -eq 'Subject Alternative Name' }; " ^
- "if ($san) { Write-Host 'SANs:' $san.Format(1) } else { Write-Host 'SANs: (none)' }"
+ REM Create temp PowerShell script for reliable execution
+ (
+ echo $file = '%%F'
+ echo $ext = [System.IO.Path]::GetExtension^($file^).ToLower^(^)
+ echo $content = Get-Content $file -Raw -ErrorAction SilentlyContinue
+ echo.
+ echo # Check for private key patterns
+ echo $isKey = $false
+ echo if ^($ext -eq '.key'^) { $isKey = $true }
+ echo elseif ^($content -match '-----BEGIN ^(RSA ^|EC ^|ENCRYPTED ^|^)PRIVATE KEY-----'^) { $isKey = $true }
+ echo elseif ^($content -match '-----BEGIN OPENSSH PRIVATE KEY-----'^) { $isKey = $true }
+ echo.
+ echo if ^($isKey^) {
+ echo Write-Host '[PRIVATE KEY] - Skipping ^(not a certificate^)' -ForegroundColor Yellow
+ echo Write-Host 'Type: Private Key file'
+ echo exit 1
+ echo }
+ echo.
+ echo # Check for PKCS#12/PFX files
+ echo if ^($ext -eq '.p12' -or $ext -eq '.pfx'^) {
+ echo Write-Host '[PKCS#12/PFX] - Contains certificate + private key bundle' -ForegroundColor Yellow
+ echo Write-Host 'Note: Use certutil or MMC to import PFX files with private keys'
+ echo exit 2
+ echo }
+ echo.
+ echo # Try to load as certificate
+ echo try {
+ echo $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2^($file^)
+ echo Write-Host '[CERTIFICATE]' -ForegroundColor Green
+ echo Write-Host 'Subject ^(CN^):' $cert.Subject
+ echo Write-Host 'Issuer:' $cert.Issuer
+ echo Write-Host 'Valid From:' $cert.NotBefore
+ echo Write-Host 'Valid To:' $cert.NotAfter
+ echo Write-Host 'Thumbprint:' $cert.Thumbprint
+ echo $san = $cert.Extensions ^| Where-Object { $_.Oid.FriendlyName -eq 'Subject Alternative Name' }
+ echo if ^($san^) { Write-Host 'SANs:' $san.Format^(1^) } else { Write-Host 'SANs: ^(none^)' }
+ echo exit 0
+ echo } catch {
+ echo Write-Host '[UNKNOWN/INVALID] - Could not parse as certificate' -ForegroundColor Red
+ echo Write-Host 'Error:' $_.Exception.Message
+ echo exit 3
+ echo }
+ ) > "%TEMP%\certcheck.ps1"
+
+ powershell -ExecutionPolicy Bypass -File "%TEMP%\certcheck.ps1"
+ set "exitcode=!errorlevel!"
echo.
- set /p "confirm=Install this certificate to Trusted Root store? (Y/N): "
- if /i "!confirm!"=="Y" (
- echo Installing %%F...
- powershell -Command "Import-Certificate -FilePath '%%F' -CertStoreLocation Cert:\CurrentUser\Root" >nul 2>&1
- if !errorlevel! equ 0 (
- echo [SUCCESS] Certificate installed.
+ REM Only prompt for installation if it's a valid certificate (exit code 0)
+ if "!exitcode!"=="0" (
+ set /p "confirm=Install this certificate to Trusted Root store? (Y/N): "
+
+ if /i "!confirm!"=="Y" (
+ echo Installing %%F...
+ powershell -Command "Import-Certificate -FilePath '%%F' -CertStoreLocation Cert:\CurrentUser\Root" >nul 2>&1
+ if !errorlevel! equ 0 (
+ echo [SUCCESS] Certificate installed.
+ set /a "certcount+=1"
+ ) else (
+ echo [ERROR] Failed to install certificate. Try running as Administrator.
+ )
) else (
- echo [ERROR] Failed to install certificate. Try running as Administrator.
+ echo Skipped %%F
)
+ ) else if "!exitcode!"=="1" (
+ set /a "keycount+=1"
+ echo [Skipped - Private key]
+ ) else if "!exitcode!"=="2" (
+ echo [Skipped - Use different tool for PFX import]
) else (
- echo Skipped %%F
+ echo [Skipped - Invalid or unrecognized file]
)
echo.
)
+REM Cleanup temp file
+del "%TEMP%\certcheck.ps1" 2>nul
+
if "!found!"=="0" (
- echo No certificate files found in current directory.
- echo Supported extensions: .cer, .crt, .pem, .der
+ echo No certificate or key files found in '%CERT_DIR%'.
+ echo Supported extensions: .cer, .crt, .pem, .der, .key, .p12, .pfx
)
+echo ============================================
+echo Summary:
+echo Certificates installed: !certcount!
+echo Private keys found (skipped): !keycount!
+echo ============================================
echo.
echo Done.
pause
+exit /b
+
+:show_help
+echo Certificate Installer - Install certificates to Windows trust store
+echo.
+echo Usage: install_cert.bat [OPTIONS] [DIRECTORY]
+echo.
+echo Arguments:
+echo DIRECTORY Folder to search for certificates (default: current directory)
+echo.
+echo Options:
+echo -h, --help, /? Show this help message and exit
+echo.
+echo Supported file types:
+echo .cer, .crt, .pem, .der - X.509 certificates (will be installed)
+echo .key - Private keys (skipped)
+echo .p12, .pfx - PKCS#12 bundles (skipped - use different tool)
+echo.
+echo Examples:
+echo install_cert.bat # Scan current directory
+echo install_cert.bat C:\path\to\certs # Scan specific directory
+echo install_cert.bat .\my-certs # Scan relative path
+echo.
+echo Note: May require Administrator privileges for system-wide installation.
+pause
+exit /b
diff --git a/extra/traefik-host-based-routing/install_cert.sh b/extra/traefik-host-based-routing/install_cert.sh
new file mode 100755
index 0000000..a55a3b2
--- /dev/null
+++ b/extra/traefik-host-based-routing/install_cert.sh
@@ -0,0 +1,229 @@
+#!/bin/bash
+
+# Show help
+show_help() {
+ cat << 'EOF'
+Certificate Installer - Install certificates to system trust store
+
+Usage: install_cert.sh [OPTIONS] [DIRECTORY]
+
+Arguments:
+ DIRECTORY Folder to search for certificates (default: current directory)
+
+Options:
+ -h, --help Show this help message and exit
+
+Supported file types:
+ .cer, .crt, .pem, .der - X.509 certificates (will be installed)
+ .key - Private keys (skipped)
+ .p12, .pfx - PKCS#12 bundles (skipped - use different tool)
+
+Examples:
+ install_cert.sh # Scan current directory
+ install_cert.sh /path/to/certs # Scan specific directory
+ install_cert.sh ./my-certs # Scan relative path
+
+Note: Requires sudo privileges for system-wide certificate installation.
+EOF
+ exit 0
+}
+
+# Parse arguments
+if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
+ show_help
+fi
+
+# Optional argument: folder to search for certificates (default: current directory)
+CERT_DIR="${1:-.}"
+
+# Resolve to absolute path and check if exists
+if [ ! -d "$CERT_DIR" ]; then
+ echo "Error: Directory '$CERT_DIR' not found."
+ echo "Use --help for usage information."
+ exit 1
+fi
+
+echo "============================================"
+echo " Certificate Installer - Root Trust Store"
+echo "============================================"
+echo ""
+echo " Scanning directory: $CERT_DIR"
+echo ""
+echo " WARNING: This script installs certificates"
+echo " into your system's trusted root store."
+echo ""
+echo " This is intended for custom self-signed"
+echo " certificates from TRUSTED sources only."
+echo ""
+echo " *** INSTALLING UNKNOWN CERTIFICATES IS ***"
+echo " *** EXTREMELY DANGEROUS! ***"
+echo ""
+echo " A malicious root certificate can allow"
+echo " attackers to intercept ALL your encrypted"
+echo " traffic, including passwords, banking,"
+echo " and personal data."
+echo ""
+echo " Only proceed if you know and trust the"
+echo " source of these certificates!"
+echo "============================================"
+echo ""
+read -p "Do you want to continue? (Y/N): " proceed
+
+if [[ ! "$proceed" =~ ^[Yy]$ ]]; then
+ echo "Aborted."
+ exit 0
+fi
+
+echo ""
+echo "Scanning for certificate and key files..."
+echo ""
+
+found=0
+certcount=0
+keycount=0
+
+# Detect OS for correct certificate installation path
+install_cert() {
+ local certfile="$1"
+
+ if [ -f /etc/debian_version ]; then
+ # Debian/Ubuntu
+ sudo cp "$certfile" /usr/local/share/ca-certificates/
+ sudo update-ca-certificates
+ elif [ -f /etc/redhat-release ]; then
+ # RHEL/CentOS/Fedora
+ sudo cp "$certfile" /etc/pki/ca-trust/source/anchors/
+ sudo update-ca-trust
+ elif [ -f /etc/arch-release ]; then
+ # Arch Linux
+ sudo cp "$certfile" /etc/ca-certificates/trust-source/anchors/
+ sudo trust extract-compat
+ elif [ -f /etc/alpine-release ]; then
+ # Alpine Linux
+ sudo cp "$certfile" /usr/local/share/ca-certificates/
+ sudo update-ca-certificates
+ elif [[ "$OSTYPE" == "darwin"* ]]; then
+ # macOS
+ sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "$certfile"
+ else
+ echo "[ERROR] Unknown OS. Please install manually."
+ return 1
+ fi
+ return 0
+}
+
+for file in "$CERT_DIR"/*.cer "$CERT_DIR"/*.crt "$CERT_DIR"/*.pem "$CERT_DIR"/*.der "$CERT_DIR"/*.key "$CERT_DIR"/*.p12 "$CERT_DIR"/*.pfx; do
+ # Skip if no files match the pattern
+ [ -e "$file" ] || continue
+
+ found=1
+ echo "--------------------------------------------"
+ echo "File: $file"
+ echo "--------------------------------------------"
+
+ ext="${file##*.}"
+ ext=$(echo "$ext" | tr '[:upper:]' '[:lower:]')
+
+ # Check for private key
+ is_key=false
+
+ if [ "$ext" = "key" ]; then
+ is_key=true
+ elif [ -f "$file" ] && grep -q -E -- "-----BEGIN (RSA |EC |ENCRYPTED )?PRIVATE KEY-----|-----BEGIN OPENSSH PRIVATE KEY-----" "$file" 2>/dev/null; then
+ is_key=true
+ fi
+
+ if [ "$is_key" = true ]; then
+ echo -e "\033[33m[PRIVATE KEY]\033[0m - Skipping (not a certificate)"
+ echo "Type: Private Key file"
+ echo ""
+ echo "[Skipped - Private key]"
+ ((keycount++))
+ echo ""
+ continue
+ fi
+
+ # Check for PKCS#12/PFX files
+ if [ "$ext" = "p12" ] || [ "$ext" = "pfx" ]; then
+ echo -e "\033[33m[PKCS#12/PFX]\033[0m - Contains certificate + private key bundle"
+ echo "Note: Use 'openssl pkcs12' to extract certificate, or import directly with browser"
+ echo ""
+ echo "[Skipped - Use different tool for PFX import]"
+ echo ""
+ continue
+ fi
+
+ # Try to parse as certificate using openssl
+ cert_info=$(openssl x509 -in "$file" -noout -subject -issuer -dates -fingerprint -ext subjectAltName 2>/dev/null)
+
+ if [ $? -eq 0 ]; then
+ echo -e "\033[32m[CERTIFICATE]\033[0m"
+
+ # Extract and display certificate details
+ subject=$(openssl x509 -in "$file" -noout -subject 2>/dev/null | sed 's/subject=/Subject (CN): /')
+ issuer=$(openssl x509 -in "$file" -noout -issuer 2>/dev/null | sed 's/issuer=/Issuer: /')
+ startdate=$(openssl x509 -in "$file" -noout -startdate 2>/dev/null | sed 's/notBefore=/Valid From: /')
+ enddate=$(openssl x509 -in "$file" -noout -enddate 2>/dev/null | sed 's/notAfter=/Valid To: /')
+ fingerprint=$(openssl x509 -in "$file" -noout -fingerprint -sha256 2>/dev/null | sed 's/sha256 Fingerprint=/Thumbprint (SHA256): /' | sed 's/SHA256 Fingerprint=/Thumbprint (SHA256): /')
+ san=$(openssl x509 -in "$file" -noout -ext subjectAltName 2>/dev/null | grep -v "X509v3 Subject Alternative Name:")
+
+ echo "$subject"
+ echo "$issuer"
+ echo "$startdate"
+ echo "$enddate"
+ echo "$fingerprint"
+
+ if [ -n "$san" ]; then
+ echo "SANs:$san"
+ else
+ echo "SANs: (none)"
+ fi
+
+ echo ""
+ read -p "Install this certificate to Trusted Root store? (Y/N): " confirm
+
+ if [[ "$confirm" =~ ^[Yy]$ ]]; then
+ echo "Installing $file..."
+
+ # Convert to PEM if needed (for DER format)
+ if [ "$ext" = "der" ]; then
+ tmpfile=$(mktemp)
+ openssl x509 -in "$file" -inform DER -out "$tmpfile" -outform PEM
+ install_cert "$tmpfile"
+ result=$?
+ rm -f "$tmpfile"
+ else
+ install_cert "$file"
+ result=$?
+ fi
+
+ if [ $result -eq 0 ]; then
+ echo "[SUCCESS] Certificate installed."
+ ((certcount++))
+ else
+ echo "[ERROR] Failed to install certificate. Make sure you have sudo privileges."
+ fi
+ else
+ echo "Skipped $file"
+ fi
+ else
+ echo -e "\033[31m[UNKNOWN/INVALID]\033[0m - Could not parse as certificate"
+ echo "Error: File is not a valid X.509 certificate or format not recognized"
+ echo ""
+ echo "[Skipped - Invalid or unrecognized file]"
+ fi
+ echo ""
+done
+
+if [ "$found" -eq 0 ]; then
+ echo "No certificate or key files found in '$CERT_DIR'."
+ echo "Supported extensions: .cer, .crt, .pem, .der, .key, .p12, .pfx"
+fi
+
+echo "============================================"
+echo "Summary:"
+echo " Certificates installed: $certcount"
+echo " Private keys found (skipped): $keycount"
+echo "============================================"
+echo ""
+echo "Done."
diff --git a/extra/traefik-host-based-routing/start.sh b/extra/traefik-host-based-routing/start.sh
index 42289a2..1845689 100755
--- a/extra/traefik-host-based-routing/start.sh
+++ b/extra/traefik-host-based-routing/start.sh
@@ -1,5 +1,6 @@
#!/bin/bash
-# Start JupyterHub platform with latest upstream
+# Start JupyterHub platform
+# Usage: ./start.sh [--refresh]
set -e
@@ -9,6 +10,14 @@ REPO_URL="https://github.com/stellarshenson/stellars-jupyterhub-ds.git"
REPO_DIR="stellars-jupyterhub-ds"
REFRESH=false
+# Default configuration (override via .env)
+ENABLE_CIFS="${ENABLE_CIFS:-0}"
+
+# Load environment variables if .env exists
+if [[ -f .env ]]; then
+ source .env
+fi
+
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
@@ -37,9 +46,16 @@ else
git clone "$REPO_URL"
fi
+# Build compose command with optional CIFS mount
+COMPOSE_FILES="-f stellars-jupyterhub-ds/compose.yml -f compose_override.yml"
+if [[ "${ENABLE_CIFS}" == "1" ]]; then
+ echo "CIFS mount enabled"
+ COMPOSE_FILES="${COMPOSE_FILES} -f compose_cifs.yml"
+fi
+
echo "Starting JupyterHub platform..."
-docker compose -f stellars-jupyterhub-ds/compose.yml -f compose_override.yml pull
+docker compose ${COMPOSE_FILES} pull
docker pull stellars/stellars-jupyterlab-ds:latest
-docker compose -f stellars-jupyterhub-ds/compose.yml -f compose_override.yml up -d --no-build
+docker compose ${COMPOSE_FILES} up -d --no-build
echo "Done. Access: https://jupyterhub.YOURDOMAIN/"
diff --git a/extra/traefik-host-based-routing/stop.sh b/extra/traefik-host-based-routing/stop.sh
index 787d8dd..d35ffe0 100755
--- a/extra/traefik-host-based-routing/stop.sh
+++ b/extra/traefik-host-based-routing/stop.sh
@@ -3,7 +3,23 @@
set -e
+cd "$(dirname "$0")"
+
+# Default configuration (override via .env)
+ENABLE_CIFS="${ENABLE_CIFS:-0}"
+
+# Load environment variables if .env exists
+if [[ -f .env ]]; then
+ source .env
+fi
+
+# Build compose command with optional CIFS mount
+COMPOSE_FILES="-f stellars-jupyterhub-ds/compose.yml -f compose_override.yml"
+if [[ "${ENABLE_CIFS}" == "1" ]]; then
+ COMPOSE_FILES="${COMPOSE_FILES} -f compose_cifs.yml"
+fi
+
echo "Stopping services..."
-docker compose -f stellars-jupyterhub-ds/compose.yml -f compose_override.yml down
+docker compose ${COMPOSE_FILES} down
echo "Done."