Add /api/certificates/request endpoint for programmatic certificate requests, update docs and add test script
All checks were successful
HAProxy Manager Build and Push / Build-and-Push (push) Successful in 37s

This commit is contained in:
jknapp 2025-07-11 17:14:01 -07:00
parent 7b0b4c0476
commit ef488a253d
4 changed files with 394 additions and 4 deletions

View File

@ -205,6 +205,47 @@ Authorization: Bearer your-api-key
## New Certificate Management Endpoints
### Request Certificate Generation
Request certificate generation for one or more domains.
```bash
POST /api/certificates/request
Authorization: Bearer your-api-key
Content-Type: application/json
{
"domains": ["example.com", "api.example.com"],
"force_renewal": false,
"include_www": true
}
# Response
{
"status": "completed",
"summary": {
"total": 2,
"successful": 2,
"failed": 0
},
"results": [
{
"domain": "example.com",
"status": "success",
"message": "Certificate obtained successfully",
"cert_path": "/etc/haproxy/certs/example.com.pem",
"domains_covered": ["example.com", "www.example.com"]
},
{
"domain": "api.example.com",
"status": "success",
"message": "Certificate obtained successfully",
"cert_path": "/etc/haproxy/certs/api.example.com.pem",
"domains_covered": ["api.example.com"]
}
]
}
```
### Renew All Certificates
Trigger renewal of all Let's Encrypt certificates and reload HAProxy.
@ -321,6 +362,16 @@ curl -X POST http://localhost:8000/api/ssl \
curl -X POST http://localhost:8000/api/certificates/renew \
-H "Authorization: Bearer your-secure-api-key-here"
# Request certificate generation for another service
curl -X POST http://localhost:8000/api/certificates/request \
-H "Authorization: Bearer your-secure-api-key-here" \
-H "Content-Type: application/json" \
-d '{
"domains": ["api.example.com"],
"force_renewal": false,
"include_www": false
}'
# Download certificate for another service
curl -H "Authorization: Bearer your-secure-api-key-here" \
http://localhost:8000/api/certificates/example.com/download \

View File

@ -23,7 +23,19 @@ This document summarizes the new features and improvements added to the HAProxy
- Returns detailed status of renewal process
- **Error Handling**: Comprehensive error logging and status reporting
### 3. Certificate Download Endpoints
### 3. Certificate Request API
- **Endpoint**: `POST /api/certificates/request`
- **Functionality**:
- Request certificate generation for one or more domains
- Support for multiple domains in a single request
- Optional www subdomain inclusion
- Force renewal option
- Automatic domain addition to database if not exists
- Batch processing with detailed results
- **Use Case**: Allow other services to request certificate generation through the HAProxy service
- **Response**: Detailed status for each domain with success/failure information
### 4. Certificate Download Endpoints
- **Endpoints**:
- `GET /api/certificates/<domain>/download` - Combined certificate (cert + key)
- `GET /api/certificates/<domain>/key` - Private key only
@ -31,7 +43,7 @@ This document summarizes the new features and improvements added to the HAProxy
- **Use Case**: Allow other services to securely download certificates for their own use
- **Security**: All endpoints require API key authentication
### 4. Certificate Status Monitoring
### 5. Certificate Status Monitoring
- **Endpoint**: `GET /api/certificates/status`
- **Functionality**:
- Lists all certificates with expiration dates
@ -39,7 +51,7 @@ This document summarizes the new features and improvements added to the HAProxy
- Provides certificate file paths
- Enables proactive certificate management
### 5. Comprehensive Error Logging and Alerting
### 6. Comprehensive Error Logging and Alerting
- **Logging System**:
- Structured JSON logging for all operations
- Separate error log file (`/var/log/haproxy-manager-errors.log`)
@ -143,6 +155,7 @@ This document summarizes the new features and improvements added to the HAProxy
- `GET /api/reload` - Reload HAProxy
### New Endpoints
- `POST /api/certificates/request` - Request certificate generation for domains
- `POST /api/certificates/renew` - Renew all certificates
- `GET /api/certificates/status` - Get certificate status
- `GET /api/certificates/<domain>/download` - Download combined certificate

View File

@ -289,6 +289,7 @@ def index():
@app.route('/api/ssl', methods=['POST'])
@require_api_key
def request_ssl():
"""Legacy endpoint for requesting SSL certificate for a single domain"""
data = request.get_json()
domain = data.get('domain')
@ -310,6 +311,9 @@ def request_ssl():
key_path = f'/etc/letsencrypt/live/{domain}/privkey.pem'
combined_path = f'{SSL_CERTS_DIR}/{domain}.pem'
# Ensure SSL certs directory exists
os.makedirs(SSL_CERTS_DIR, exist_ok=True)
with open(combined_path, 'w') as combined:
subprocess.run(['cat', cert_path, key_path], stdout=combined)
@ -326,7 +330,12 @@ def request_ssl():
conn.close()
generate_config()
log_operation('request_ssl', True, f'SSL certificate obtained for {domain}')
return jsonify({'status': 'success'})
return jsonify({
'status': 'success',
'domain': domain,
'cert_path': combined_path,
'message': 'Certificate obtained successfully'
})
else:
error_msg = f'Failed to obtain SSL certificate: {result.stderr}'
log_operation('request_ssl', False, error_msg)
@ -491,6 +500,137 @@ def get_certificate_status():
log_operation('get_certificate_status', False, str(e))
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/api/certificates/request', methods=['POST'])
@require_api_key
def request_certificates():
"""Request certificate generation for one or more domains"""
data = request.get_json()
domains = data.get('domains', [])
force_renewal = data.get('force_renewal', False)
include_www = data.get('include_www', True)
if not domains:
log_operation('request_certificates', False, 'No domains provided')
return jsonify({'status': 'error', 'message': 'At least one domain is required'}), 400
if not isinstance(domains, list):
domains = [domains] # Convert single domain to list
results = []
success_count = 0
error_count = 0
for domain in domains:
try:
# Prepare domain list for certbot (include www subdomain if requested)
certbot_domains = [domain]
if include_www and not domain.startswith('www.'):
certbot_domains.append(f'www.{domain}')
# Build certbot command
cmd = [
'certbot', 'certonly', '-n', '--standalone',
'--preferred-challenges', 'http', '--http-01-port=8688'
]
if force_renewal:
cmd.append('--force-renewal')
# Add domains
for d in certbot_domains:
cmd.extend(['-d', d])
# Request certificate
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
# Combine cert files and copy to HAProxy certs directory
cert_path = f'/etc/letsencrypt/live/{domain}/fullchain.pem'
key_path = f'/etc/letsencrypt/live/{domain}/privkey.pem'
combined_path = f'{SSL_CERTS_DIR}/{domain}.pem'
# Ensure SSL certs directory exists
os.makedirs(SSL_CERTS_DIR, exist_ok=True)
with open(combined_path, 'w') as combined:
subprocess.run(['cat', cert_path, key_path], stdout=combined)
# Update database (add domain if it doesn't exist)
with sqlite3.connect(DB_FILE) as conn:
cursor = conn.cursor()
# Check if domain exists
cursor.execute('SELECT id FROM domains WHERE domain = ?', (domain,))
domain_exists = cursor.fetchone()
if domain_exists:
# Update existing domain
cursor.execute('''
UPDATE domains
SET ssl_enabled = 1, ssl_cert_path = ?
WHERE domain = ?
''', (combined_path, domain))
else:
# Add new domain with SSL enabled
cursor.execute('''
INSERT INTO domains (domain, ssl_enabled, ssl_cert_path)
VALUES (?, 1, ?)
''', (domain, combined_path))
results.append({
'domain': domain,
'status': 'success',
'message': 'Certificate obtained successfully',
'cert_path': combined_path,
'domains_covered': certbot_domains
})
success_count += 1
else:
error_msg = f'Failed to obtain certificate for {domain}: {result.stderr}'
results.append({
'domain': domain,
'status': 'error',
'message': error_msg,
'stderr': result.stderr
})
error_count += 1
except Exception as e:
error_msg = f'Exception while processing {domain}: {str(e)}'
results.append({
'domain': domain,
'status': 'error',
'message': error_msg
})
error_count += 1
# Regenerate HAProxy config if any certificates were successful
if success_count > 0:
try:
generate_config()
log_operation('request_certificates', True, f'Successfully obtained {success_count} certificates, {error_count} failed')
except Exception as e:
log_operation('request_certificates', False, f'Certificates obtained but config generation failed: {str(e)}')
# Return results
response = {
'status': 'completed',
'summary': {
'total': len(domains),
'successful': success_count,
'failed': error_count
},
'results': results
}
if error_count == 0:
return jsonify(response), 200
elif success_count > 0:
return jsonify(response), 207 # Multi-status (some succeeded, some failed)
else:
return jsonify(response), 500 # All failed
@app.route('/api/domain', methods=['DELETE'])
@require_api_key
def remove_domain():

View File

@ -0,0 +1,186 @@
#!/bin/bash
# HAProxy Manager Certificate Request Test Script
# This script tests the new certificate request endpoint
BASE_URL="http://localhost:8000"
API_KEY="${HAPROXY_API_KEY:-}"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to print colored output
print_status() {
local status=$1
local message=$2
case $status in
"PASS")
echo -e "${GREEN}✓ PASS${NC}: $message"
;;
"FAIL")
echo -e "${RED}✗ FAIL${NC}: $message"
;;
"INFO")
echo -e "${BLUE} INFO${NC}: $message"
;;
"WARN")
echo -e "${YELLOW}⚠ WARN${NC}: $message"
;;
esac
}
# Function to make API request
api_request() {
local method=$1
local endpoint=$2
local data=$3
local headers=""
if [ -n "$API_KEY" ]; then
headers="-H \"Authorization: Bearer $API_KEY\""
fi
if [ -n "$data" ]; then
headers="$headers -H \"Content-Type: application/json\" -d '$data'"
fi
eval "curl -s -w \"%{http_code}\" -o /tmp/cert_request_response.json $headers -X $method $BASE_URL$endpoint"
}
# Test single domain certificate request
test_single_domain_request() {
print_status "INFO" "Testing single domain certificate request..."
local test_domain="test-$(date +%s).example.com"
local data="{\"domains\": [\"$test_domain\"], \"force_renewal\": false, \"include_www\": false}"
local response=$(api_request "POST" "/api/certificates/request" "$data")
local status_code=$(echo "$response" | tail -c 4)
if [ "$status_code" = "200" ] || [ "$status_code" = "207" ] || [ "$status_code" = "401" ]; then
print_status "PASS" "Single domain request endpoint responded (status: $status_code)"
if [ "$status_code" != "401" ]; then
# Parse response
local success_count=$(jq -r '.summary.successful' /tmp/cert_request_response.json 2>/dev/null)
local failed_count=$(jq -r '.summary.failed' /tmp/cert_request_response.json 2>/dev/null)
if [ "$success_count" = "1" ]; then
print_status "PASS" "Certificate request successful for $test_domain"
elif [ "$failed_count" = "1" ]; then
print_status "WARN" "Certificate request failed for $test_domain (expected for test domain)"
else
print_status "FAIL" "Unexpected response format"
fi
fi
else
print_status "FAIL" "Single domain request failed with status $status_code"
fi
}
# Test multiple domain certificate request
test_multiple_domain_request() {
print_status "INFO" "Testing multiple domain certificate request..."
local test_domains="[\"test1-$(date +%s).example.com\", \"test2-$(date +%s).example.com\"]"
local data="{\"domains\": $test_domains, \"force_renewal\": false, \"include_www\": true}"
local response=$(api_request "POST" "/api/certificates/request" "$data")
local status_code=$(echo "$response" | tail -c 4)
if [ "$status_code" = "200" ] || [ "$status_code" = "207" ] || [ "$status_code" = "401" ]; then
print_status "PASS" "Multiple domain request endpoint responded (status: $status_code)"
if [ "$status_code" != "401" ]; then
local total=$(jq -r '.summary.total' /tmp/cert_request_response.json 2>/dev/null)
if [ "$total" = "2" ]; then
print_status "PASS" "Multiple domain request processed correctly"
else
print_status "FAIL" "Multiple domain request response format error"
fi
fi
else
print_status "FAIL" "Multiple domain request failed with status $status_code"
fi
}
# Test certificate request with force renewal
test_force_renewal_request() {
print_status "INFO" "Testing certificate request with force renewal..."
local test_domain="test-force-$(date +%s).example.com"
local data="{\"domains\": [\"$test_domain\"], \"force_renewal\": true, \"include_www\": false}"
local response=$(api_request "POST" "/api/certificates/request" "$data")
local status_code=$(echo "$response" | tail -c 4)
if [ "$status_code" = "200" ] || [ "$status_code" = "207" ] || [ "$status_code" = "401" ]; then
print_status "PASS" "Force renewal request endpoint responded (status: $status_code)"
else
print_status "FAIL" "Force renewal request failed with status $status_code"
fi
}
# Test invalid request (no domains)
test_invalid_request() {
print_status "INFO" "Testing invalid request (no domains)..."
local data="{\"domains\": [], \"force_renewal\": false, \"include_www\": false}"
local response=$(api_request "POST" "/api/certificates/request" "$data")
local status_code=$(echo "$response" | tail -c 4)
if [ "$status_code" = "400" ] || [ "$status_code" = "401" ]; then
print_status "PASS" "Invalid request properly rejected (status: $status_code)"
else
print_status "FAIL" "Invalid request not properly rejected (status: $status_code)"
fi
}
# Test certificate status endpoint
test_certificate_status() {
print_status "INFO" "Testing certificate status endpoint..."
local response=$(api_request "GET" "/api/certificates/status")
local status_code=$(echo "$response" | tail -c 4)
if [ "$status_code" = "200" ] || [ "$status_code" = "401" ]; then
print_status "PASS" "Certificate status endpoint responded (status: $status_code)"
if [ "$status_code" != "401" ]; then
local cert_count=$(jq -r '.certificates | length' /tmp/cert_request_response.json 2>/dev/null)
print_status "INFO" "Found $cert_count certificates in status"
fi
else
print_status "FAIL" "Certificate status failed with status $status_code"
fi
}
# Main test execution
main() {
echo "HAProxy Manager Certificate Request Test Suite"
echo "=============================================="
echo "Base URL: $BASE_URL"
echo "API Key: ${API_KEY:-"Not configured"}"
echo ""
test_invalid_request
test_single_domain_request
test_multiple_domain_request
test_force_renewal_request
test_certificate_status
echo ""
echo "Test completed. Check /tmp/cert_request_response.json for detailed responses."
echo ""
echo "Note: Certificate requests for test domains will likely fail as they don't"
echo "resolve to this server. This is expected behavior for testing."
}
# Run tests
main "$@"