CGI Scripts¶
Navigator supports running CGI (Common Gateway Interface) scripts directly, without needing to start a full web application. This is ideal for lightweight endpoints that run standalone scripts, such as database synchronization, configuration updates, or status checks.
Overview¶
CGI scripts provide a way to execute standalone scripts in response to HTTP requests. Navigator's CGI support includes:
- User switching: Run scripts as specific Unix users (requires root)
- Method filtering: Restrict scripts to specific HTTP methods
- Environment variables: Pass custom environment to scripts
- Automatic reload: Reload configuration after script execution
- Timeout control: Set execution time limits
Configuration¶
Add CGI scripts to the server section of your configuration:
server:
listen: 3000
cgi_scripts:
- path: /admin/sync
script: /opt/app/script/sync_databases.rb
method: POST
user: appuser
group: appgroup
timeout: 5m
reload_config: config/navigator.yml
env:
RAILS_DB_VOLUME: /mnt/db
RAILS_ENV: production
Configuration Options¶
| Field | Type | Required | Description |
|---|---|---|---|
path |
string | Yes | URL path to match (exact match) |
script |
string | Yes | Absolute path to the executable CGI script |
method |
string | No | HTTP method restriction (GET, POST, etc.). Empty = all methods |
user |
string | No | Unix user to run script as (empty = current user) |
group |
string | No | Unix group to run script as (empty = user's primary group) |
allowed_users |
list | No | Usernames allowed to access this script. Empty = all authenticated users |
env |
map | No | Additional environment variables to set |
reload_config |
string | No | Config file to reload after successful execution |
timeout |
string | No | Execution timeout (e.g., "30s", "5m"). Zero = no timeout |
Example: Showcase Database Sync¶
Here's how to configure Navigator to handle Showcase's database sync endpoints without starting a Rails tenant:
server:
listen: 3000
hostname: smooth.fly.dev
cgi_scripts:
# Database sync endpoint - updates index database from S3
- path: /showcase/index_update
script: /rails/script/sync_databases_s3.rb
method: POST
user: rails
group: rails
timeout: 10m
reload_config: config/navigator.yml
env:
RAILS_DB_VOLUME: /mnt/db
RAILS_ENV: production
AWS_REGION: us-east-1
# Index modification time check
- path: /showcase/index_date
script: /rails/script/check_index_date.rb
method: GET
user: rails
timeout: 5s
env:
RAILS_DB_VOLUME: /mnt/db
CGI Environment Variables¶
Navigator automatically sets standard CGI environment variables (RFC 3875):
| Variable | Description |
|---|---|
GATEWAY_INTERFACE |
Always "CGI/1.1" |
SERVER_PROTOCOL |
HTTP protocol version (e.g., "HTTP/1.1") |
SERVER_SOFTWARE |
Always "Navigator" |
REQUEST_METHOD |
HTTP method (GET, POST, etc.) |
QUERY_STRING |
URL query parameters |
SCRIPT_NAME |
Request path |
SERVER_NAME |
Server hostname |
REMOTE_ADDR |
Client IP address |
CONTENT_TYPE |
Request Content-Type header |
CONTENT_LENGTH |
Request Content-Length header |
HTTP_* |
All HTTP headers as HTTP_* variables |
Additionally, Navigator passes all custom environment variables defined in the env section.
Script Requirements¶
CGI scripts must:
- Be executable: Set execute permission (
chmod +x script.rb) - Have shebang: First line should be
#!/usr/bin/env ruby(or appropriate interpreter) - Output CGI headers: Print headers followed by blank line before body
- Exit with status: Exit 0 for success, non-zero for error
Example Script¶
#!/usr/bin/env ruby
# Read environment
db_volume = ENV['RAILS_DB_VOLUME'] || 'db'
# Perform work
result = sync_databases(db_volume)
# Output CGI response
puts "Content-Type: text/plain"
puts "Status: 200 OK"
puts ""
puts "Sync completed: #{result}"
exit 0
Status Codes¶
CGI scripts can set HTTP status codes via the Status header:
User Switching (Unix Only)¶
CGI scripts can run as different users for security isolation:
cgi_scripts:
- path: /admin/backup
script: /opt/scripts/backup.sh
user: backup # Run as 'backup' user
group: backup # Run as 'backup' group
Requirements: - Navigator must be running as root - The specified user and group must exist on the system - Works on Unix/Linux only (not Windows)
Without user switching: - Scripts run as the same user as Navigator - Simpler but less secure
Configuration Reload¶
The reload_config feature automatically reloads Navigator's configuration after successful script execution:
cgi_scripts:
- path: /admin/update
script: /opt/scripts/update_htpasswd.rb
reload_config: config/navigator.yml
How It Works¶
Configuration reload is triggered only if:
1. The reload_config field is specified, AND
2. Either:
- The config file path is different from the currently loaded config, OR
- The config file was modified during script execution
This avoids unnecessary reloads when nothing has changed.
Note: CGI scripts and lifecycle hooks share the same smart reload logic, ensuring consistent behavior across Navigator.
Use Cases¶
- Authentication updates: Reload after updating htpasswd file
- Tenant changes: Reload after adding/removing tenants
- Dynamic configuration: Scripts that generate new configuration
Example: Update Authentication¶
cgi_scripts:
- path: /admin/update_users
script: /opt/scripts/update_users.rb
method: POST
user: admin
reload_config: config/navigator.yml
env:
HTPASSWD_FILE: /etc/navigator/htpasswd
#!/usr/bin/env ruby
# Update htpasswd file
htpasswd_file = ENV['HTPASSWD_FILE']
update_users(htpasswd_file)
# Output success
puts "Content-Type: text/plain"
puts ""
puts "Users updated. Configuration will be reloaded."
# Exit success - Navigator will reload config automatically
exit 0
Request Routing¶
CGI scripts are evaluated in the request handling order:
- Health check endpoint (
/up) - Authentication check
- Rewrites and redirects
- CGI scripts ← You are here
- Reverse proxies
- Static files
- Web application proxy
This means:
- CGI scripts are subject to authentication (unless path is in public_paths)
- CGI scripts run before reverse proxies
- CGI scripts take precedence over web applications
Timeout Handling¶
Set timeouts to prevent long-running scripts from blocking:
If a script exceeds its timeout: - The script process is terminated - HTTP 500 error is returned to the client - Error is logged
Error Handling¶
When a CGI script fails:
- Non-zero exit: HTTP 500 returned, stderr logged
- Timeout: Process killed, HTTP 500 returned
- Not found: Error logged at startup, requests return 404
- Permission denied: Error logged at startup
Check Navigator logs for CGI execution details:
level=INFO msg="Executing CGI script" script=/opt/sync.rb method=POST path=/admin/sync user=appuser
level=INFO msg="CGI script completed" script=/opt/sync.rb duration=1.2s
level=ERROR msg="CGI script execution failed" script=/opt/sync.rb error="exit status 1"
Security Considerations¶
User Permissions¶
When running scripts as different users:
cgi_scripts:
- path: /admin/restricted
script: /opt/scripts/admin_task.rb
user: admin # Runs with elevated privileges
Best practices: - Run scripts with minimum required privileges - Don't run scripts as root unless absolutely necessary - Use Navigator's authentication to control access - Validate all script inputs
Authentication¶
CGI scripts respect Navigator's authentication and support fine-grained access control:
auth:
enabled: true
htpasswd: /etc/navigator/htpasswd
public_paths:
- /public/*
- /health
cgi_scripts:
# Restricted to specific users
- path: /admin/sync
script: /opt/sync.rb
allowed_users:
- admin
- operator
# Available to all authenticated users
- path: /admin/status
script: /opt/status.rb
# Public endpoint (no authentication required)
- path: /public/health
script: /opt/health.sh
Access Control Behavior:
- With
allowed_users: Only specified usernames can access the script (returns 403 Forbidden for other authenticated users) - Without
allowed_users: All authenticated users can access the script - Public paths: Scripts on paths listed in
public_pathscan be accessed without authentication
Example: Multi-level access control
auth:
enabled: true
htpasswd: /etc/navigator/htpasswd
cgi_scripts:
# Admin-only: Database operations
- path: /admin/db_sync
script: /opt/scripts/sync_db.rb
allowed_users:
- admin
# Operators can restart services
- path: /admin/restart
script: /opt/scripts/restart_service.sh
allowed_users:
- admin
- operator
- oncall
# All authenticated users can check status
- path: /admin/status
script: /opt/scripts/check_status.sh
# No allowed_users = all authenticated users
Performance¶
CGI scripts start a new process for each request:
- Startup cost: Fork + exec overhead (~5-50ms)
- Memory: Each execution is independent
- Scalability: Suitable for low-to-medium traffic
When to use CGI: - Infrequent operations (database sync, admin tasks) - Lightweight checks (status, health) - One-off scripts that don't justify a full web app
When not to use CGI: - High-frequency endpoints - Real-time applications - WebSocket connections - Long-running connections
Comparison with Web Applications¶
| Feature | CGI Scripts | Web Applications |
|---|---|---|
| Startup | Per request | Once, then reused |
| Memory | Minimal | Higher (persistent process) |
| Performance | Good for infrequent | Good for frequent |
| State | Stateless | Can maintain state |
| Use case | Admin tasks, checks | Full applications |
Examples¶
Database Status Check¶
#!/bin/sh
if [ -f "$RAILS_DB_VOLUME/index.sqlite3" ]; then
mtime=$(stat -f %Sm -t %Y-%m-%dT%H:%M:%SZ "$RAILS_DB_VOLUME/index.sqlite3")
echo "Content-Type: text/plain"
echo ""
echo "OK: Last modified $mtime"
exit 0
else
echo "Status: 503 Service Unavailable"
echo "Content-Type: text/plain"
echo ""
echo "ERROR: Database not found"
exit 1
fi
Webhook Handler¶
cgi_scripts:
- path: /webhooks/github
script: /opt/scripts/github_webhook.rb
method: POST
user: webhook
timeout: 30s
env:
GITHUB_SECRET: "${GITHUB_WEBHOOK_SECRET}"
Configuration Generator¶
cgi_scripts:
- path: /admin/generate_config
script: /opt/scripts/generate_config.rb
method: POST
user: admin
reload_config: config/navigator.yml
Troubleshooting¶
Script Not Executing¶
- Check execute permission:
ls -l /path/to/script.cgi - Verify shebang is correct:
head -1 /path/to/script.cgi - Test script manually:
sudo -u appuser /path/to/script.cgi - Check Navigator logs for errors
Permission Denied¶
- Navigator must run as root for user switching
- Script file must be readable by target user
- Script directory must be accessible
Script Times Out¶
- Increase timeout:
timeout: 10m - Optimize script performance
- Consider moving to web application if consistently slow
Config Not Reloading¶
- Verify
reload_configpath is correct - Check file modification time after script runs
- Ensure script modifies config before exiting
- Look for reload messages in logs
See Also¶
- Lifecycle Hooks - Server and tenant lifecycle automation
- Configuration Reference - Full YAML configuration options
- Authentication - Securing CGI endpoints