├── modules ├── __init__.py ├── web │ └── __init__.py ├── api │ └── __init__.py └── core │ ├── __init__.py │ ├── cache.py │ ├── shell.py │ ├── auth.py │ ├── rate_limit.py │ ├── ocsp_crl.py │ └── audit.py ├── .coverage ├── CONTRIBUTING.md ├── favicon.ico ├── screenshot_1.png ├── screenshot_2.png ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── ci.yml │ └── docker-multiplatform.yml.disabled ├── requirements-vault-storage.txt ├── requirements-aws-storage.txt ├── requirements-infisical-storage.txt ├── requirements-azure-storage.txt ├── requirements-test.txt ├── requirements-aws.txt ├── requirements-gcp.txt ├── requirements-azure.txt ├── requirements-storage-all.txt ├── run-docker.sh ├── requirements-minimal.txt ├── start.sh ├── run-tests.sh ├── .env.example ├── requirements-extended.txt ├── certmate.service ├── pytest.ini ├── .dockerignore ├── LICENSE ├── nginx.conf ├── .gitignore ├── quick-fix.sh ├── debug-docker.sh ├── docker-compose.yml ├── requirements.txt ├── .env.template ├── setup.sh ├── Dockerfile ├── test_dns_accounts.py ├── test_actual_settings.py ├── Makefile ├── test_dns_provider.py ├── test_route53_cert.py ├── test-multiplatform.sh ├── debug_storage_test.py ├── test_certificate_listing.py ├── debug_storage_simple.py ├── test_dns_provider_inheritance.py ├── debug_response.py ├── MODULAR_STRUCTURE.md ├── CODE_OF_CONDUCT.md ├── test_certificate_creation.py ├── test_shell_executor.py ├── docs ├── CHANGELOG.md ├── README.md └── index.md ├── test_dns_provider_detection.py ├── DOCKER_DEPLOYMENT.md ├── INSTALLATION.md ├── API_TESTING.md ├── TESTING.md ├── test_domain_alias.py ├── DOCKER_MULTIPLATFORM.md └── CA_PROVIDERS.md /modules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.coverage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabriziosalmi/certmate/HEAD/.coverage -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Just create a PR anytime to contribute to the project. 2 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabriziosalmi/certmate/HEAD/favicon.ico -------------------------------------------------------------------------------- /screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabriziosalmi/certmate/HEAD/screenshot_1.png -------------------------------------------------------------------------------- /screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabriziosalmi/certmate/HEAD/screenshot_2.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: fabriziosalmi 4 | -------------------------------------------------------------------------------- /requirements-vault-storage.txt: -------------------------------------------------------------------------------- 1 | # Optional dependencies for HashiCorp Vault storage backend 2 | hvac>=1.1.0 3 | -------------------------------------------------------------------------------- /requirements-aws-storage.txt: -------------------------------------------------------------------------------- 1 | # Optional dependencies for AWS Secrets Manager storage backend 2 | boto3>=1.26.0 3 | -------------------------------------------------------------------------------- /requirements-infisical-storage.txt: -------------------------------------------------------------------------------- 1 | # Optional dependencies for Infisical storage backend 2 | infisical-python>=2.0.0 3 | -------------------------------------------------------------------------------- /requirements-azure-storage.txt: -------------------------------------------------------------------------------- 1 | # Optional dependencies for Azure Key Vault storage backend 2 | azure-keyvault-secrets>=4.7.0 3 | azure-identity>=1.12.0 4 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov 3 | pytest-mock 4 | pytest-asyncio 5 | pytest-flask 6 | pytest-xdist 7 | coverage 8 | requests-mock 9 | freezegun -------------------------------------------------------------------------------- /requirements-aws.txt: -------------------------------------------------------------------------------- 1 | # AWS/Route53 specific requirements 2 | # Install this in addition to requirements-minimal.txt for AWS support 3 | 4 | boto3==1.34.144 5 | certbot-dns-route53==2.11.0 6 | -------------------------------------------------------------------------------- /requirements-gcp.txt: -------------------------------------------------------------------------------- 1 | # Google Cloud DNS specific requirements 2 | # Install this in addition to requirements-minimal.txt for Google Cloud support 3 | 4 | google-cloud-dns==0.35.0 5 | certbot-dns-google==2.11.0 6 | -------------------------------------------------------------------------------- /requirements-azure.txt: -------------------------------------------------------------------------------- 1 | # Azure DNS specific requirements 2 | # Install this in addition to requirements-minimal.txt for Azure support 3 | 4 | azure-identity==1.17.1 5 | azure-mgmt-dns==8.1.0 6 | certbot-dns-azure==2.11.0 7 | -------------------------------------------------------------------------------- /modules/web/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Web module for CertMate 3 | Contains web interface routes and form-based endpoints 4 | """ 5 | 6 | from .routes import register_web_routes 7 | 8 | __all__ = [ 9 | 'register_web_routes' 10 | ] 11 | -------------------------------------------------------------------------------- /modules/api/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | API module for CertMate 3 | Contains API models and resources for the REST API 4 | """ 5 | 6 | from .models import create_api_models 7 | from .resources import create_api_resources 8 | 9 | __all__ = [ 10 | 'create_api_models', 11 | 'create_api_resources' 12 | ] 13 | -------------------------------------------------------------------------------- /requirements-storage-all.txt: -------------------------------------------------------------------------------- 1 | # All optional storage backend dependencies 2 | # Install with: pip install -r requirements-storage-all.txt 3 | 4 | # Azure Key Vault 5 | azure-keyvault-secrets>=4.7.0 6 | azure-identity>=1.12.0 7 | 8 | # AWS Secrets Manager 9 | boto3>=1.26.0 10 | 11 | # HashiCorp Vault 12 | hvac>=1.1.0 13 | 14 | # Infisical 15 | infisical-python>=2.0.0 16 | -------------------------------------------------------------------------------- /run-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Docker run script with proper volume mounts 3 | 4 | docker run -d \ 5 | --name certmate \ 6 | -p 8000:8000 \ 7 | -v "$(pwd)/certificates:/app/certificates" \ 8 | -v "$(pwd)/data:/app/data" \ 9 | -v "$(pwd)/logs:/app/logs" \ 10 | -e FLASK_ENV=production \ 11 | -e API_BEARER_TOKEN="${API_BEARER_TOKEN}" \ 12 | certmate:latest 13 | -------------------------------------------------------------------------------- /requirements-minimal.txt: -------------------------------------------------------------------------------- 1 | # Minimal CertMate installation - Cloudflare only (fastest install) 2 | # Use this for quick testing or Cloudflare-only deployments 3 | 4 | # Core Flask dependencies 5 | Flask==3.0.3 6 | Flask-CORS==6.0.0 7 | flask-restx==1.3.0 8 | 9 | # Certificate management 10 | certbot==2.10.0 11 | josepy==1.13.0 12 | certbot-dns-cloudflare==2.10.0 13 | 14 | # Cloudflare API support 15 | cloudflare==2.19.4 16 | 17 | # Core application dependencies 18 | requests==2.32.4 19 | python-dotenv==1.0.1 20 | APScheduler==3.10.4 21 | cryptography==44.0.1 22 | pyopenssl==24.1.0 23 | 24 | # Production server 25 | gunicorn==23.0.0 26 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # CertMate Startup Script 4 | 5 | echo "🛡️ Starting CertMate SSL Certificate Manager..." 6 | echo "" 7 | 8 | # Check if virtual environment exists 9 | if [ ! -d ".venv" ]; then 10 | echo "❌ Virtual environment not found. Please run setup first." 11 | exit 1 12 | fi 13 | 14 | # Activate virtual environment and start the application 15 | source .venv/bin/activate 16 | echo "✅ Virtual environment activated" 17 | echo "🚀 Starting Flask application..." 18 | echo "" 19 | echo "Open your browser and go to: http://localhost:8000" 20 | echo "Press Ctrl+C to stop the application" 21 | echo "" 22 | 23 | python app.py 24 | -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Simple test runner for CertMate 4 | echo "🧪 Running CertMate tests..." 5 | 6 | # Set test environment 7 | export FLASK_ENV=testing 8 | export TESTING=true 9 | 10 | # Run tests 11 | echo "Running test suite..." 12 | pytest -v --tb=short 13 | 14 | if [ $? -eq 0 ]; then 15 | echo "✅ All tests passed!" 16 | 17 | # Run with coverage if requested 18 | if [ "$1" = "--coverage" ]; then 19 | echo "📊 Generating coverage report..." 20 | pytest --cov=. --cov-report=term-missing --cov-report=html 21 | echo "Coverage report saved to htmlcov/index.html" 22 | fi 23 | else 24 | echo "❌ Some tests failed!" 25 | exit 1 26 | fi 27 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # CertMate Configuration 2 | # Copy this file to .env and update the values 3 | 4 | # Flask Configuration 5 | FLASK_ENV=production 6 | FLASK_DEBUG=False 7 | 8 | # Application Settings 9 | HOST=0.0.0.0 10 | PORT=8000 11 | 12 | # Cloudflare API Token 13 | # Get this from: https://dash.cloudflare.com/profile/api-tokens 14 | # Required permissions: Zone:DNS:Edit, Zone:Zone:Read 15 | CLOUDFLARE_TOKEN=your_cloudflare_api_token_here 16 | 17 | # API Bearer Token for authentication 18 | # Use a strong, random token for security 19 | # This token will be required for all API calls 20 | API_BEARER_TOKEN=your_secure_api_token_here 21 | 22 | # Optional: Set a secret key for session management 23 | SECRET_KEY=your-secret-key-here 24 | -------------------------------------------------------------------------------- /requirements-extended.txt: -------------------------------------------------------------------------------- 1 | # Extended DNS providers for specialized use cases 2 | # Install this in addition to requirements-minimal.txt for additional DNS providers 3 | 4 | # Self-hosted and enterprise DNS 5 | certbot-dns-powerdns==2.11.0 6 | certbot-dns-rfc2136==2.11.0 7 | 8 | # Popular hosting providers 9 | certbot-dns-linode==2.11.0 10 | certbot-dns-vultr==2.11.0 11 | certbot-dns-hetzner==2.11.0 12 | 13 | # Domain registrars with DNS 14 | certbot-dns-gandi==2.11.0 15 | certbot-dns-ovh==2.11.0 16 | certbot-dns-namecheap==2.11.0 17 | certbot-dns-porkbun==2.11.0 18 | certbot-dns-godaddy==2.11.0 19 | 20 | # Specialized DNS services 21 | certbot-dns-dnsmadeeasy==2.11.0 22 | certbot-dns-nsone==2.11.0 23 | certbot-dns-he-ddns==2.11.0 24 | certbot-dns-dynudns==2.11.0 25 | -------------------------------------------------------------------------------- /certmate.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=CertMate SSL Certificate Manager 3 | Documentation=https://github.com/fabriziosalmi/certmate 4 | After=network.target network-online.target 5 | Wants=network-online.target 6 | 7 | [Service] 8 | Type=simple 9 | User=certmate 10 | Group=certmate 11 | WorkingDirectory=/opt/certmate 12 | Environment=PATH=/opt/certmate/venv/bin 13 | Environment=API_BEARER_TOKEN=change-this-secure-token 14 | ExecStart=/opt/certmate/venv/bin/gunicorn --bind 0.0.0.0:8000 --workers 4 app:app 15 | ExecReload=/bin/kill -s HUP $MAINPID 16 | Restart=always 17 | RestartSec=10 18 | KillMode=mixed 19 | TimeoutStopSec=5 20 | 21 | # Security settings 22 | NoNewPrivileges=true 23 | ProtectSystem=strict 24 | ProtectHome=true 25 | ReadWritePaths=/opt/certmate/certificates /opt/certmate/data 26 | PrivateTmp=true 27 | ProtectKernelTunables=true 28 | ProtectKernelModules=true 29 | ProtectControlGroups=true 30 | 31 | [Install] 32 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = -v --tb=short --strict-markers --strict-config 3 | testpaths = tests 4 | python_files = test_*.py 5 | python_functions = test_* 6 | python_classes = Test* 7 | log_cli_level = INFO 8 | log_file_level = INFO 9 | log_file = pytest.log 10 | markers = 11 | unit: Unit tests 12 | integration: Integration tests 13 | slow: Slow running tests 14 | api: API tests 15 | dns: DNS provider tests 16 | filterwarnings = 17 | ignore::DeprecationWarning 18 | ignore::PendingDeprecationWarning 19 | 20 | [coverage:run] 21 | branch = True 22 | source = . 23 | omit = 24 | tests/* 25 | venv/* 26 | .venv/* 27 | __pycache__/* 28 | .pytest_cache/* 29 | 30 | [coverage:report] 31 | show_missing = True 32 | skip_covered = False 33 | precision = 2 34 | exclude_lines = 35 | pragma: no cover 36 | def __repr__ 37 | raise AssertionError 38 | raise NotImplementedError 39 | if __name__ == .__main__.: -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Exclude sensitive environment files 2 | .env 3 | .env.* 4 | *.env 5 | 6 | # Exclude development files 7 | .git 8 | .gitignore 9 | .github 10 | .vscode 11 | .idea 12 | 13 | # Exclude documentation and metadata 14 | README.md 15 | *.md 16 | screenshot_*.png 17 | CODE_OF_CONDUCT.md 18 | CONTRIBUTING.md 19 | DNS_PROVIDERS.md 20 | INSTALLATION.md 21 | LICENSE 22 | 23 | # Exclude Python cache and temporary files 24 | __pycache__/ 25 | *.pyc 26 | *.pyo 27 | *.pyd 28 | .Python 29 | *.so 30 | .pytest_cache 31 | .coverage 32 | .tox 33 | .cache 34 | 35 | # Exclude local development data 36 | certificates/ 37 | data/ 38 | letsencrypt/ 39 | logs/ 40 | 41 | # Exclude Docker and deployment files 42 | docker-compose.yml 43 | Dockerfile 44 | .dockerignore 45 | 46 | # Exclude test files 47 | test_*.py 48 | *_test.py 49 | 50 | # Exclude setup and validation scripts 51 | setup.sh 52 | start.sh 53 | validate_dependencies.py 54 | api_client_example.py 55 | 56 | # Exclude any backup files 57 | *.bak 58 | *.backup 59 | *.old 60 | *.orig 61 | 62 | # Exclude OS specific files 63 | .DS_Store 64 | Thumbs.db 65 | 66 | backups/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Fabrizio Salmi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 1024; 3 | } 4 | 5 | http { 6 | upstream certmate { 7 | server certmate:8000; 8 | } 9 | 10 | server { 11 | listen 80; 12 | server_name _; 13 | 14 | location / { 15 | proxy_pass http://certmate; 16 | proxy_set_header Host $host; 17 | proxy_set_header X-Real-IP $remote_addr; 18 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 19 | proxy_set_header X-Forwarded-Proto $scheme; 20 | } 21 | } 22 | 23 | # HTTPS configuration (optional) 24 | server { 25 | listen 443 ssl; 26 | server_name _; 27 | 28 | ssl_certificate /etc/nginx/ssl/cert.pem; 29 | ssl_certificate_key /etc/nginx/ssl/key.pem; 30 | 31 | location / { 32 | proxy_pass http://certmate; 33 | proxy_set_header Host $host; 34 | proxy_set_header X-Real-IP $remote_addr; 35 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 36 | proxy_set_header X-Forwarded-Proto $scheme; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | MANIFEST 23 | 24 | # Virtual Environment 25 | venv/ 26 | .venv/ 27 | env/ 28 | ENV/ 29 | 30 | # Application specific 31 | settings.json 32 | cloudflare.ini 33 | certificates/ 34 | data/ 35 | logs/ 36 | *.pem 37 | *.key 38 | *.crt 39 | *.csr 40 | letsencrypt/ 41 | .secrets/ 42 | 43 | # Environment files 44 | .env 45 | .env.local 46 | .env.production 47 | 48 | # IDE 49 | .vscode/ 50 | .idea/ 51 | *.swp 52 | *.swo 53 | 54 | # OS 55 | .DS_Store 56 | Thumbs.db 57 | 58 | # Logs 59 | *.log 60 | 61 | # Backup files 62 | *_old.* 63 | *_backup.* 64 | 65 | # Planning and development documents 66 | NEXT_FEATURES.md 67 | !MULTI_ACCOUNT_EXAMPLES.md 68 | 69 | htmlcov/ 70 | .pytest_cache/ 71 | 72 | backups/ 73 | app.py.backup 74 | 75 | # Test files with sensitive data 76 | test_real_cert_*.py 77 | test_api_*.py 78 | test_certificate_creation_*.py 79 | real_world_test_*.py 80 | integration_test_*.py -------------------------------------------------------------------------------- /quick-fix.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Quick fix for existing certificates not showing in Docker 3 | 4 | echo "🔧 Quick fix for certificates not showing in Docker..." 5 | 6 | # Stop any running containers 7 | docker-compose down 2>/dev/null || true 8 | 9 | # Check local certificates 10 | local_domains=$(find ./certificates -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l) 11 | echo "📁 Local domains found: $local_domains" 12 | 13 | if [ $local_domains -gt 0 ]; then 14 | echo "📋 Local certificate domains:" 15 | ls -1 certificates/ 16 | fi 17 | 18 | # Start with docker-compose 19 | echo "🚀 Starting with docker-compose..." 20 | docker-compose up -d 21 | 22 | # Wait and check 23 | sleep 5 24 | 25 | if docker-compose ps | grep -q "Up"; then 26 | echo "✅ Container is running" 27 | 28 | # Check container certificates 29 | echo "🔍 Checking certificates in container..." 30 | docker-compose exec certmate ls -la /app/certificates 31 | 32 | echo "" 33 | echo "🌐 Open http://localhost:8000 to see your certificates" 34 | echo "🔑 Check /app/data/settings.json for API token if needed" 35 | 36 | else 37 | echo "❌ Container failed to start. Checking logs..." 38 | docker-compose logs certmate 39 | fi 40 | -------------------------------------------------------------------------------- /debug-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Debug script to check certificate visibility in Docker container 3 | 4 | echo "=== Local Certificate Check ===" 5 | echo "Certificates in local folder:" 6 | ls -la certificates/ 2>/dev/null || echo "No certificates folder found locally" 7 | 8 | echo "" 9 | echo "=== Docker Container Check ===" 10 | if docker ps | grep -q certmate; then 11 | echo "CertMate container is running. Checking certificates inside container:" 12 | docker exec certmate ls -la /app/certificates 2>/dev/null || echo "Cannot access container certificates" 13 | 14 | echo "" 15 | echo "Checking container data directory:" 16 | docker exec certmate ls -la /app/data 2>/dev/null || echo "Cannot access container data" 17 | 18 | echo "" 19 | echo "Checking if container can read settings:" 20 | docker exec certmate cat /app/data/settings.json 2>/dev/null || echo "Cannot read settings from container" 21 | else 22 | echo "CertMate container is not running" 23 | echo "Available containers:" 24 | docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}" 25 | fi 26 | 27 | echo "" 28 | echo "=== Docker Compose Service Check ===" 29 | if command -v docker-compose >/dev/null 2>&1; then 30 | docker-compose ps 2>/dev/null || echo "No docker-compose services running" 31 | else 32 | echo "docker-compose not available" 33 | fi 34 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | 2 | services: 3 | certmate: 4 | build: 5 | context: . 6 | args: 7 | REQUIREMENTS_FILE: requirements.txt 8 | # Uncomment to use pre-built multi-platform image instead of building locally: 9 | # image: YOUR_DOCKERHUB_USERNAME/certmate:latest 10 | # platform: linux/amd64 # Optional: force specific platform (linux/amd64, linux/arm64) 11 | container_name: certmate 12 | ports: 13 | - "8000:8000" 14 | environment: 15 | - FLASK_ENV=production 16 | - SECRET_KEY=${SECRET_KEY:-your-secret-key-here} 17 | - CLOUDFLARE_TOKEN=${CLOUDFLARE_TOKEN} 18 | - API_BEARER_TOKEN=${API_BEARER_TOKEN} 19 | volumes: 20 | - ./certificates:/app/certificates:rw 21 | - ./logs:/app/logs:rw 22 | - ./data:/app/data:rw 23 | - ./backups:/app/backups:rw 24 | restart: unless-stopped 25 | healthcheck: 26 | test: ["CMD", "curl", "-f", "http://localhost:8000/health"] 27 | interval: 30s 28 | timeout: 10s 29 | retries: 3 30 | start_period: 40s 31 | 32 | # Optional: Add nginx reverse proxy 33 | nginx: 34 | image: nginx:alpine 35 | container_name: certmate-nginx 36 | ports: 37 | - "80:80" 38 | - "443:443" 39 | volumes: 40 | - ./nginx.conf:/etc/nginx/nginx.conf:ro 41 | - ./certificates:/etc/nginx/ssl:ro 42 | depends_on: 43 | - certmate 44 | restart: unless-stopped 45 | profiles: 46 | - nginx 47 | 48 | networks: 49 | default: 50 | name: certmate_network 51 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # CertMate - Complete SSL Certificate Management System 2 | # All DNS providers and cloud SDKs included for maximum compatibility 3 | 4 | # Core Flask dependencies 5 | Flask==3.0.3 6 | Flask-CORS==6.0.0 7 | flask-restx==1.3.0 8 | 9 | # Certificate management 10 | certbot==2.10.0 11 | josepy==1.13.0 12 | 13 | # DNS provider plugins - Major cloud providers 14 | certbot-dns-cloudflare==2.10.0 15 | certbot-dns-route53==2.10.0 16 | certbot-dns-digitalocean==2.10.0 17 | certbot-dns-google==2.10.0 18 | 19 | # DNS provider plugins - Additional providers 20 | certbot-dns-powerdns 21 | certbot-dns-linode 22 | certbot-dns-gandi 23 | certbot-dns-ovh 24 | certbot-dns-namecheap 25 | certbot-dns-rfc2136 26 | certbot-dns-vultr 27 | certbot-dns-dnsmadeeasy 28 | certbot-dns-nsone 29 | certbot-dns-hetzner 30 | certbot-dns-porkbun 31 | certbot-dns-godaddy 32 | certbot-dns-arvancloud 33 | certbot-acme-dns 34 | 35 | # Cloud SDK dependencies 36 | boto3==1.34.144 # AWS support 37 | azure-identity==1.17.1 # Azure support 38 | azure-mgmt-dns==8.1.0 # Azure DNS support 39 | google-cloud-dns==0.35.0 # Google Cloud support 40 | cloudflare==2.19.4 # Cloudflare API support 41 | 42 | # Core application dependencies 43 | requests==2.32.4 44 | python-dotenv==1.0.1 45 | APScheduler==3.10.4 46 | cryptography==45.0.0 47 | pyopenssl==25.1.0 48 | 49 | # Production server 50 | gunicorn==23.0.0 51 | 52 | # Monitoring and metrics 53 | prometheus_client==0.21.0 54 | 55 | # Testing 56 | requests_mock 57 | -------------------------------------------------------------------------------- /modules/core/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Core module for CertMate 3 | Contains core functionality including file operations, settings, authentication, 4 | certificate management, DNS providers, cache management, and storage backends 5 | """ 6 | 7 | from .file_operations import FileOperations 8 | from .settings import SettingsManager 9 | from .auth import AuthManager 10 | from .certificates import CertificateManager 11 | from .dns_providers import DNSManager 12 | from .cache import CacheManager 13 | from .storage_backends import ( 14 | StorageManager, 15 | CertificateStorageBackend, 16 | LocalFileSystemBackend, 17 | AzureKeyVaultBackend, 18 | AWSSecretsManagerBackend, 19 | HashiCorpVaultBackend, 20 | InfisicalBackend 21 | ) 22 | from .private_ca import PrivateCAGenerator 23 | from .csr_handler import CSRHandler 24 | from .client_certificates import ClientCertificateManager 25 | from .ocsp_crl import OCSPResponder, CRLManager 26 | from .audit import AuditLogger 27 | from .rate_limit import RateLimitConfig, SimpleRateLimiter, rate_limit_decorator 28 | 29 | __all__ = [ 30 | 'FileOperations', 31 | 'SettingsManager', 32 | 'AuthManager', 33 | 'CertificateManager', 34 | 'DNSManager', 35 | 'CacheManager', 36 | 'StorageManager', 37 | 'CertificateStorageBackend', 38 | 'LocalFileSystemBackend', 39 | 'AzureKeyVaultBackend', 40 | 'AWSSecretsManagerBackend', 41 | 'HashiCorpVaultBackend', 42 | 'InfisicalBackend', 43 | 'PrivateCAGenerator', 44 | 'CSRHandler', 45 | 'ClientCertificateManager', 46 | 'OCSPResponder', 47 | 'CRLManager', 48 | 'AuditLogger', 49 | 'RateLimitConfig', 50 | 'SimpleRateLimiter', 51 | 'rate_limit_decorator' 52 | ] 53 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | # CertMate Environment Configuration Template 2 | # Copy this file to .env and update with your actual values 3 | # DO NOT commit this file with real values to version control 4 | 5 | # Flask Configuration 6 | FLASK_ENV=production 7 | FLASK_DEBUG=False 8 | 9 | # Application Settings 10 | HOST=0.0.0.0 11 | PORT=8000 12 | LOG_LEVEL=INFO 13 | 14 | # Security Configuration 15 | # Generate a strong secret key for Flask sessions 16 | # You can generate one with: python -c "import secrets; print(secrets.token_urlsafe(32))" 17 | SECRET_KEY=your-super-secret-key-here-change-this 18 | 19 | # API Authentication 20 | # Generate a strong admin token for API access 21 | # You can generate one with: python -c "import secrets; print(secrets.token_urlsafe(32))" 22 | ADMIN_TOKEN=your-admin-token-here-change-this 23 | 24 | # Cloudflare Configuration 25 | # Get your API token from: https://dash.cloudflare.com/profile/api-tokens 26 | # Required permissions: Zone:DNS:Edit, Zone:Zone:Read 27 | CLOUDFLARE_EMAIL=your-email@example.com 28 | CLOUDFLARE_API_TOKEN=your-cloudflare-api-token-here 29 | 30 | # Optional: Other DNS Provider Configurations 31 | # Uncomment and configure if using other DNS providers 32 | 33 | # Route53 (AWS) 34 | # AWS_ACCESS_KEY_ID=your-aws-access-key 35 | # AWS_SECRET_ACCESS_KEY=your-aws-secret-key 36 | # AWS_DEFAULT_REGION=us-east-1 37 | 38 | # Azure DNS 39 | # AZURE_CLIENT_ID=your-azure-client-id 40 | # AZURE_CLIENT_SECRET=your-azure-client-secret 41 | # AZURE_TENANT_ID=your-azure-tenant-id 42 | # AZURE_SUBSCRIPTION_ID=your-azure-subscription-id 43 | 44 | # Google Cloud DNS 45 | # GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json 46 | 47 | # PowerDNS 48 | # POWERDNS_API_URL=https://your-powerdns-server.com:8081 49 | # POWERDNS_API_KEY=your-powerdns-api-key 50 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # CertMate Setup Script 4 | 5 | echo "🛡️ Setting up CertMate SSL Certificate Manager..." 6 | echo "" 7 | 8 | # Check if Python 3 is installed 9 | if ! command -v python3 &> /dev/null; then 10 | echo "❌ Python 3 is not installed. Please install Python 3.8 or higher." 11 | exit 1 12 | fi 13 | 14 | echo "✅ Python 3 found: $(python3 --version)" 15 | 16 | # Create virtual environment 17 | if [ ! -d ".venv" ]; then 18 | echo "📦 Creating virtual environment..." 19 | python3 -m venv .venv 20 | echo "✅ Virtual environment created" 21 | else 22 | echo "✅ Virtual environment already exists" 23 | fi 24 | 25 | # Activate virtual environment 26 | source .venv/bin/activate 27 | echo "✅ Virtual environment activated" 28 | 29 | # Install dependencies 30 | echo "📦 Installing Python dependencies..." 31 | pip install -r requirements.txt 32 | echo "✅ Dependencies installed" 33 | 34 | # Check if certbot is installed 35 | if ! command -v certbot &> /dev/null; then 36 | echo "" 37 | echo "⚠️ certbot is not installed globally." 38 | echo "Please install certbot using one of these methods:" 39 | echo "" 40 | echo "macOS (with Homebrew):" 41 | echo " brew install certbot" 42 | echo "" 43 | echo "Ubuntu/Debian:" 44 | echo " sudo apt-get install certbot python3-certbot-dns-cloudflare" 45 | echo "" 46 | echo "CentOS/RHEL:" 47 | echo " sudo yum install certbot python3-certbot-dns-cloudflare" 48 | echo "" 49 | else 50 | echo "✅ certbot found: $(certbot --version)" 51 | fi 52 | 53 | echo "" 54 | echo "🎉 Setup complete!" 55 | echo "" 56 | echo "Next steps (if you have DNS at Cloudflare):" 57 | echo "1. Get your Cloudflare API token from: https://dash.cloudflare.com/profile/api-tokens" 58 | echo "2. Run: ./start.sh" 59 | echo "3. Open http://localhost:8000 in your browser" 60 | echo "4. Go to Settings and configure your API token and email" 61 | echo "5. Same process can be used for others DNS providers" 62 | echo "" 63 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Multi-stage build for optimized image size and faster builds 2 | FROM python:3.11-slim AS builder 3 | 4 | # Set working directory for build stage 5 | WORKDIR /build 6 | 7 | # Install build dependencies 8 | RUN apt-get update && apt-get install -y \ 9 | gcc \ 10 | && rm -rf /var/lib/apt/lists/* 11 | 12 | # Copy requirements first for better caching 13 | COPY requirements.txt ./ 14 | 15 | # Create virtual environment and install dependencies 16 | RUN python -m venv /opt/venv 17 | ENV PATH="/opt/venv/bin:$PATH" 18 | 19 | # Install minimal requirements by default (fastest build) 20 | # Override with --build-arg REQUIREMENTS_FILE=requirements.txt for full install 21 | ARG REQUIREMENTS_FILE=requirements.txt 22 | RUN pip install -U pip wheel && \ 23 | pip install --no-cache-dir -r ${REQUIREMENTS_FILE} 24 | 25 | # Production stage 26 | FROM python:3.11-slim 27 | 28 | # Set working directory 29 | WORKDIR /app 30 | 31 | # Install runtime dependencies only 32 | RUN apt-get update && apt-get install -y \ 33 | curl \ 34 | && rm -rf /var/lib/apt/lists/* \ 35 | && useradd --create-home --shell /bin/bash certmate 36 | 37 | # Copy virtual environment from builder stage 38 | COPY --from=builder /opt/venv /opt/venv 39 | ENV PATH="/opt/venv/bin:$PATH" 40 | 41 | # Copy application code 42 | COPY . . 43 | 44 | # Create necessary directories with proper permissions 45 | RUN mkdir -p certificates data logs backups && \ 46 | chown -R certmate:certmate /app 47 | 48 | # Ensure proper permissions for volume mounts 49 | RUN chmod 755 /app/certificates /app/data /app/logs 50 | 51 | # Set environment variables 52 | ENV FLASK_APP=app.py 53 | ENV FLASK_ENV=production 54 | ENV PYTHONPATH=/app 55 | 56 | # Switch to non-root user 57 | USER certmate 58 | 59 | # Expose port 60 | EXPOSE 8000 61 | 62 | # Health check 63 | HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ 64 | CMD curl -f http://localhost:8000/health || exit 1 65 | 66 | # Run the application 67 | CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "--timeout", "360", "app:app"] 68 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main, develop ] 6 | pull_request: 7 | branches: [ main, develop ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: [3.9, 3.11, 3.12] 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v4 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | 26 | - name: Cache pip dependencies 27 | uses: actions/cache@v3 28 | with: 29 | path: ~/.cache/pip 30 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} 31 | restore-keys: | 32 | ${{ runner.os }}-pip- 33 | 34 | - name: Install dependencies 35 | run: | 36 | python -m pip install --upgrade pip 37 | pip install -r requirements.txt 38 | pip install -r requirements-test.txt 39 | 40 | - name: Lint with flake8 41 | run: | 42 | pip install flake8 43 | # stop the build if there are Python syntax errors or undefined names 44 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 45 | # exit-zero treats all errors as warnings 46 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 47 | 48 | - name: Security check with bandit 49 | run: | 50 | pip install bandit 51 | bandit -r . --severity-level medium || true 52 | 53 | - name: Run tests with coverage 54 | env: 55 | FLASK_ENV: testing 56 | TESTING: true 57 | run: | 58 | pytest -v --tb=short --cov=. --cov-report=xml --cov-report=html 59 | 60 | - name: Upload coverage reports 61 | if: matrix.python-version == '3.11' 62 | uses: codecov/codecov-action@v3 63 | with: 64 | file: ./coverage.xml 65 | flags: unittests 66 | name: codecov-umbrella 67 | 68 | - name: Test Docker build 69 | if: matrix.python-version == '3.11' 70 | run: | 71 | docker build -t certmate:test . -------------------------------------------------------------------------------- /test_dns_accounts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test script to verify DNS provider account configurations 4 | """ 5 | 6 | import os 7 | import sys 8 | import json 9 | from pathlib import Path 10 | 11 | # Add the current directory to Python path 12 | sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) 13 | 14 | def test_dns_provider_accounts(): 15 | """Test DNS provider account configurations""" 16 | 17 | from app import get_dns_provider_account_config, load_settings 18 | 19 | print("Testing DNS provider account configurations...") 20 | 21 | # Load your actual settings 22 | settings = load_settings() 23 | 24 | print(f"DNS Providers configured: {list(settings.get('dns_providers', {}).keys())}") 25 | print(f"Default accounts: {settings.get('default_accounts', {})}") 26 | 27 | # Test account configurations 28 | test_cases = [ 29 | ("cloudflare", "default"), 30 | ("route53", "certmate_test"), 31 | ("cloudflare", None), # Should use default 32 | ("route53", None), # Should use default 33 | ] 34 | 35 | print(f"\nAccount Configuration Tests:") 36 | for provider, account_id in test_cases: 37 | try: 38 | config, used_account_id = get_dns_provider_account_config(provider, account_id, settings) 39 | if config: 40 | # Mask sensitive information 41 | safe_config = {} 42 | for key, value in config.items(): 43 | if key in ['api_token', 'secret_access_key', 'client_secret']: 44 | safe_config[key] = f"***{value[-4:]}" if value else "not_set" 45 | else: 46 | safe_config[key] = value 47 | 48 | print(f" ✓ {provider} (account: {account_id}) -> {used_account_id}") 49 | print(f" Config: {safe_config}") 50 | else: 51 | print(f" ✗ {provider} (account: {account_id}) -> No configuration found") 52 | except Exception as e: 53 | print(f" ✗ {provider} (account: {account_id}) -> Error: {e}") 54 | 55 | print("\nTest completed!") 56 | 57 | if __name__ == '__main__': 58 | test_dns_provider_accounts() 59 | -------------------------------------------------------------------------------- /test_actual_settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test script to verify DNS provider detection with actual settings 4 | """ 5 | 6 | import os 7 | import sys 8 | import json 9 | from pathlib import Path 10 | 11 | # Add the current directory to Python path 12 | sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) 13 | 14 | def test_with_actual_settings(): 15 | """Test DNS provider detection with actual settings file""" 16 | 17 | from app import get_domain_dns_provider, load_settings 18 | 19 | print("Testing DNS provider detection with actual settings...") 20 | 21 | # Load your actual settings 22 | settings = load_settings() 23 | 24 | print(f"Global DNS provider: {settings.get('dns_provider', 'cloudflare')}") 25 | print(f"Domains in settings: {len(settings.get('domains', []))}") 26 | 27 | # Test each domain in your settings 28 | domains = settings.get('domains', []) 29 | print(f"\nDomains configuration:") 30 | for domain_entry in domains: 31 | if isinstance(domain_entry, dict): 32 | domain = domain_entry.get('domain') 33 | configured_provider = domain_entry.get('dns_provider') 34 | account_id = domain_entry.get('account_id') 35 | print(f" {domain}: {configured_provider} (account: {account_id})") 36 | else: 37 | print(f" {domain_entry}: (old format - using global provider)") 38 | 39 | # Test DNS provider detection 40 | test_domains = [ 41 | "test2.audiolibri.org", 42 | "aws-test3.test.certmate.org", 43 | "cf-test1.audiolibri.org", 44 | "cf-test2.audiolibri.org" 45 | ] 46 | 47 | print(f"\nDNS Provider Detection Results:") 48 | for domain in test_domains: 49 | detected_provider = get_domain_dns_provider(domain, settings) 50 | print(f" {domain} -> {detected_provider}") 51 | 52 | # Test some domains that don't exist in settings 53 | print(f"\nTesting non-configured domains (should use global default):") 54 | non_configured_domains = [ 55 | "unknown-domain.com", 56 | "test.example.com" 57 | ] 58 | 59 | for domain in non_configured_domains: 60 | detected_provider = get_domain_dns_provider(domain, settings) 61 | print(f" {domain} -> {detected_provider}") 62 | 63 | print("\nTest completed!") 64 | 65 | if __name__ == '__main__': 66 | test_with_actual_settings() 67 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test test-unit test-integration test-coverage test-watch install-dev lint format security check pre-commit clean help 2 | 3 | # Default target 4 | help: 5 | @echo "Available commands:" 6 | @echo " install-dev Install development dependencies" 7 | @echo " test Run all tests" 8 | @echo " test-unit Run unit tests only" 9 | @echo " test-integration Run integration tests only" 10 | @echo " test-coverage Run tests with coverage report" 11 | @echo " test-watch Run tests in watch mode" 12 | @echo " lint Run linting checks" 13 | @echo " format Format code with black and isort" 14 | @echo " security Run security checks" 15 | @echo " check Run all checks (lint, security, tests)" 16 | @echo " pre-commit Install and run pre-commit hooks" 17 | @echo " clean Clean up temporary files" 18 | 19 | # Install development dependencies 20 | install-dev: 21 | pip install -r requirements.txt 22 | pip install -r requirements-test.txt 23 | pip install pre-commit black isort flake8 bandit safety 24 | 25 | # Run all tests 26 | test: 27 | pytest 28 | 29 | # Run only unit tests 30 | test-unit: 31 | pytest -m "not integration and not slow" 32 | 33 | # Run only integration tests 34 | test-integration: 35 | pytest -m integration 36 | 37 | # Run tests with coverage 38 | test-coverage: 39 | pytest --cov=. --cov-report=html --cov-report=term-missing --cov-report=xml 40 | 41 | # Run tests in watch mode (requires pytest-watch) 42 | test-watch: 43 | pip install pytest-watch 44 | ptw 45 | 46 | # Linting 47 | lint: 48 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 49 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 50 | 51 | # Format code 52 | format: 53 | black . 54 | isort . --profile black 55 | 56 | # Security checks 57 | security: 58 | bandit -r . --severity-level medium 59 | safety check 60 | 61 | # Run all checks 62 | check: lint security test 63 | 64 | # Pre-commit setup 65 | pre-commit: 66 | pre-commit install 67 | pre-commit run --all-files 68 | 69 | # Clean up 70 | clean: 71 | find . -type f -name "*.pyc" -delete 72 | find . -type d -name "__pycache__" -delete 73 | find . -type d -name ".pytest_cache" -delete 74 | rm -rf htmlcov/ 75 | rm -rf .coverage 76 | rm -rf coverage.xml 77 | rm -rf dist/ 78 | rm -rf build/ 79 | rm -rf *.egg-info/ 80 | 81 | # CI simulation (what runs in GitHub Actions) 82 | ci: lint security test-coverage 83 | -------------------------------------------------------------------------------- /test_dns_provider.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test script to verify DNS provider detection for domains 4 | """ 5 | 6 | import os 7 | import sys 8 | import json 9 | from pathlib import Path 10 | 11 | # Add the current directory to Python path 12 | sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) 13 | 14 | def test_dns_provider_detection(): 15 | """Test that DNS providers are correctly detected for domains""" 16 | 17 | # Load settings 18 | settings_file = Path("data/settings.json") 19 | if not settings_file.exists(): 20 | print("Settings file not found!") 21 | return 22 | 23 | with open(settings_file, 'r') as f: 24 | settings = json.load(f) 25 | 26 | # Import the function we need to test 27 | from app import get_domain_dns_provider 28 | 29 | print("Testing DNS provider detection...") 30 | print(f"Global DNS provider: {settings.get('dns_provider', 'not set')}") 31 | print() 32 | 33 | # Test domains 34 | test_domains = [ 35 | "test2.audiolibri.org", 36 | "aws-test3.test.certmate.org", 37 | "cf-test1.audiolibri.org", 38 | "nonexistent.domain.com" 39 | ] 40 | 41 | for domain in test_domains: 42 | provider = get_domain_dns_provider(domain, settings) 43 | print(f"Domain: {domain}") 44 | print(f" DNS Provider: {provider}") 45 | 46 | # Check if domain is in settings 47 | domain_in_settings = False 48 | for domain_entry in settings.get('domains', []): 49 | if isinstance(domain_entry, dict): 50 | if domain_entry.get('domain') == domain: 51 | domain_in_settings = True 52 | expected_provider = domain_entry.get('dns_provider', settings.get('dns_provider', 'cloudflare')) 53 | print(f" Expected: {expected_provider}") 54 | print(f" Match: {'✓' if provider == expected_provider else '✗'}") 55 | break 56 | elif isinstance(domain_entry, str) and domain_entry == domain: 57 | domain_in_settings = True 58 | expected_provider = settings.get('dns_provider', 'cloudflare') 59 | print(f" Expected: {expected_provider} (global)") 60 | print(f" Match: {'✓' if provider == expected_provider else '✗'}") 61 | break 62 | 63 | if not domain_in_settings: 64 | expected_provider = settings.get('dns_provider', 'cloudflare') 65 | print(f" Expected: {expected_provider} (fallback)") 66 | print(f" Match: {'✓' if provider == expected_provider else '✗'}") 67 | 68 | print() 69 | 70 | if __name__ == '__main__': 71 | test_dns_provider_detection() 72 | -------------------------------------------------------------------------------- /test_route53_cert.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Quick test for Route53 certificate creation 4 | """ 5 | 6 | import os 7 | import sys 8 | import requests 9 | import random 10 | import string 11 | from datetime import datetime 12 | 13 | # Configuration 14 | BASE_URL = "http://localhost:8000" 15 | API_TOKEN = "3kQlbC4OQIcKriSVJ7zYlX6vJy8w0HIOD-YyNSSXuC4" 16 | HEADERS = { 17 | "Authorization": f"Bearer {API_TOKEN}", 18 | "Content-Type": "application/json" 19 | } 20 | 21 | def generate_random_subdomain(): 22 | """Generate a random subdomain for testing""" 23 | return ''.join(random.choices(string.ascii_lowercase + string.digits, k=8)) 24 | 25 | def log_test(message): 26 | """Log test message with timestamp""" 27 | timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") 28 | print(f"[{timestamp}] {message}") 29 | 30 | def create_route53_certificate(): 31 | """Test Route53 certificate creation""" 32 | # Generate a random subdomain 33 | subdomain = generate_random_subdomain() 34 | domain = f"{subdomain}.test.certmate.org" 35 | 36 | payload = { 37 | "domain": domain, 38 | "dns_provider": "route53", 39 | "account_id": "certmate_test", 40 | "staging": True # Use staging to avoid rate limits 41 | } 42 | 43 | log_test(f"Creating Route53 certificate for: {domain}") 44 | log_test(f"Payload: {payload}") 45 | 46 | try: 47 | response = requests.post( 48 | f"{BASE_URL}/api/certificates/create", 49 | headers=HEADERS, 50 | json=payload, 51 | timeout=300 52 | ) 53 | 54 | log_test(f"Response status: {response.status_code}") 55 | 56 | if response.status_code == 200: 57 | result = response.json() 58 | log_test(f"Response: {result}") 59 | 60 | if result.get("success"): 61 | log_test(f"✓ Route53 certificate created successfully for {domain}") 62 | return True 63 | else: 64 | log_test(f"✗ Route53 certificate creation failed: {result.get('message', 'Unknown error')}") 65 | return False 66 | else: 67 | log_test(f"✗ HTTP error: {response.status_code}") 68 | try: 69 | error_detail = response.json() 70 | log_test(f"Error details: {error_detail}") 71 | except: 72 | log_test(f"Error response: {response.text}") 73 | return False 74 | 75 | except Exception as e: 76 | log_test(f"✗ Exception: {e}") 77 | return False 78 | 79 | if __name__ == '__main__': 80 | log_test("=== Route53 Certificate Creation Test ===") 81 | success = create_route53_certificate() 82 | log_test(f"Test result: {'SUCCESS' if success else 'FAILED'}") 83 | sys.exit(0 if success else 1) 84 | -------------------------------------------------------------------------------- /modules/core/cache.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cache management module for CertMate 3 | Handles deployment status caching and cache operations 4 | """ 5 | 6 | import logging 7 | from .utils import DeploymentStatusCache 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class CacheManager: 13 | """Class to handle cache management operations""" 14 | 15 | def __init__(self, settings_manager): 16 | self.settings_manager = settings_manager 17 | self.deployment_cache = DeploymentStatusCache() 18 | self.update_cache_settings() 19 | 20 | def update_cache_settings(self): 21 | """Update cache settings from configuration""" 22 | try: 23 | settings = self.settings_manager.load_settings() 24 | cache_ttl = settings.get('cache_ttl', 300) 25 | self.deployment_cache.set_ttl(cache_ttl) 26 | logger.info(f"Updated deployment cache TTL to {cache_ttl} seconds") 27 | except Exception as e: 28 | logger.error(f"Error updating cache settings: {e}") 29 | 30 | def get_cache_stats(self): 31 | """Get cache statistics""" 32 | try: 33 | return self.deployment_cache.get_stats() 34 | except Exception as e: 35 | logger.error(f"Error getting cache stats: {e}") 36 | return { 37 | 'total_entries': 0, 38 | 'current_ttl': 300, 39 | 'entries': [] 40 | } 41 | 42 | def clear_cache(self): 43 | """Clear all cache entries""" 44 | try: 45 | cleared_count = self.deployment_cache.clear() 46 | logger.info(f"Cleared {cleared_count} cache entries") 47 | return cleared_count 48 | except Exception as e: 49 | logger.error(f"Error clearing cache: {e}") 50 | return 0 51 | 52 | def get_deployment_status(self, domain): 53 | """Get deployment status from cache""" 54 | try: 55 | return self.deployment_cache.get(domain) 56 | except Exception as e: 57 | logger.error(f"Error getting deployment status for {domain}: {e}") 58 | return None 59 | 60 | def set_deployment_status(self, domain, status): 61 | """Set deployment status in cache""" 62 | try: 63 | self.deployment_cache.set(domain, status) 64 | logger.debug(f"Set deployment status for {domain}: {status}") 65 | except Exception as e: 66 | logger.error(f"Error setting deployment status for {domain}: {e}") 67 | 68 | def remove_from_cache(self, domain): 69 | """Remove specific domain from cache""" 70 | try: 71 | self.deployment_cache.remove(domain) 72 | logger.debug(f"Removed {domain} from cache") 73 | except Exception as e: 74 | logger.error(f"Error removing {domain} from cache: {e}") 75 | 76 | def get_cache_instance(self): 77 | """Get the deployment cache instance for direct access""" 78 | return self.deployment_cache 79 | -------------------------------------------------------------------------------- /test-multiplatform.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # CertMate Multi-Platform Test Script 4 | # Tests Docker images across different architectures 5 | 6 | set -e 7 | 8 | # Colors 9 | RED='\033[0;31m' 10 | GREEN='\033[0;32m' 11 | YELLOW='\033[1;33m' 12 | BLUE='\033[0;34m' 13 | NC='\033[0m' 14 | 15 | echo -e "${BLUE}🧪 CertMate Multi-Platform Test${NC}" 16 | echo -e "${BLUE}===============================${NC}" 17 | 18 | # Configuration 19 | IMAGE_NAME="certmate:test" 20 | PLATFORMS=("linux/amd64" "linux/arm64") 21 | TEST_PORT=8080 22 | 23 | # Check if Docker Buildx is available 24 | if ! docker buildx version >/dev/null 2>&1; then 25 | echo -e "${RED}❌ Docker Buildx not available${NC}" 26 | exit 1 27 | fi 28 | 29 | echo -e "${GREEN}✅ Docker Buildx available${NC}" 30 | 31 | # Build test images for each platform 32 | echo -e "${YELLOW}🔨 Building test images...${NC}" 33 | for platform in "${PLATFORMS[@]}"; do 34 | echo "Building for $platform..." 35 | docker buildx build --platform "$platform" --load -t "${IMAGE_NAME}-$(echo $platform | tr '/' '-')" . || { 36 | echo -e "${YELLOW}⚠️ Failed to build for $platform (emulation may not be available)${NC}" 37 | continue 38 | } 39 | echo -e "${GREEN}✅ Built for $platform${NC}" 40 | done 41 | 42 | # Test each platform 43 | echo -e "${YELLOW}🧪 Testing platforms...${NC}" 44 | for platform in "${PLATFORMS[@]}"; do 45 | platform_tag=$(echo $platform | tr '/' '-') 46 | image_name="${IMAGE_NAME}-${platform_tag}" 47 | 48 | if ! docker images -q "$image_name" >/dev/null; then 49 | echo -e "${YELLOW}⏭️ Skipping $platform (image not available)${NC}" 50 | continue 51 | fi 52 | 53 | echo "Testing $platform..." 54 | 55 | # Create minimal test environment 56 | cat > .env.test << EOF 57 | API_BEARER_TOKEN=test-token-12345 58 | CLOUDFLARE_TOKEN=dummy-token 59 | FLASK_ENV=production 60 | HOST=0.0.0.0 61 | PORT=8000 62 | EOF 63 | 64 | # Start container 65 | container_id=$(docker run -d --env-file .env.test -p $TEST_PORT:8000 "$image_name") 66 | 67 | # Wait for startup 68 | echo "Waiting for container to start..." 69 | sleep 10 70 | 71 | # Test health endpoint 72 | if curl -sf "http://localhost:$TEST_PORT/health" >/dev/null; then 73 | echo -e "${GREEN}✅ Health check passed for $platform${NC}" 74 | 75 | # Test API endpoint 76 | if curl -sf -H "Authorization: Bearer test-token-12345" "http://localhost:$TEST_PORT/api/certificates" >/dev/null; then 77 | echo -e "${GREEN}✅ API check passed for $platform${NC}" 78 | else 79 | echo -e "${YELLOW}⚠️ API check failed for $platform${NC}" 80 | fi 81 | else 82 | echo -e "${RED}❌ Health check failed for $platform${NC}" 83 | fi 84 | 85 | # Get container info 86 | arch=$(docker exec "$container_id" uname -m 2>/dev/null || echo "unknown") 87 | echo "Container architecture: $arch" 88 | 89 | # Cleanup 90 | docker stop "$container_id" >/dev/null 91 | docker rm "$container_id" >/dev/null 92 | 93 | # Increment port for next test 94 | ((TEST_PORT++)) 95 | done 96 | 97 | # Cleanup 98 | rm -f .env.test 99 | 100 | echo "" 101 | echo -e "${GREEN}🎉 Multi-platform testing completed!${NC}" 102 | echo "" 103 | echo -e "${BLUE}💡 To build and push multi-platform images:${NC}" 104 | echo " ./build-multiplatform.sh -r YOUR_DOCKERHUB_USERNAME -p" 105 | echo "" 106 | echo -e "${BLUE}💡 To run on specific platform:${NC}" 107 | echo " docker run --platform linux/amd64 certmate:test" 108 | echo " docker run --platform linux/arm64 certmate:test" 109 | -------------------------------------------------------------------------------- /debug_storage_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Debug script to test storage backend endpoint response 4 | """ 5 | 6 | import sys 7 | import os 8 | import tempfile 9 | 10 | # Add the parent directory to the path so we can import the modules 11 | sys.path.insert(0, os.path.dirname(__file__)) 12 | 13 | def test_storage_backend_endpoint(): 14 | """Test the storage backend endpoint directly""" 15 | print("Testing storage backend endpoint...") 16 | 17 | # Set up test environment 18 | test_dir = tempfile.mkdtemp() 19 | os.environ['TESTING'] = 'True' 20 | os.environ['FLASK_ENV'] = 'testing' 21 | os.environ['SECRET_KEY'] = 'test-secret-key-12345' 22 | 23 | from app import app as flask_app 24 | import json 25 | 26 | # Configure the app for testing 27 | flask_app.config.update({ 28 | 'TESTING': True, 29 | 'WTF_CSRF_ENABLED': False, 30 | 'SECRET_KEY': 'test-secret-key-12345', 31 | 'CERT_DIR': os.path.join(test_dir, 'certificates'), 32 | 'DATA_DIR': os.path.join(test_dir, 'data'), 33 | }) 34 | 35 | # Create test directories 36 | os.makedirs(flask_app.config['CERT_DIR'], exist_ok=True) 37 | os.makedirs(flask_app.config['DATA_DIR'], exist_ok=True) 38 | 39 | # Create a test settings file with the token 40 | test_settings = { 41 | "api_bearer_token": "test-api-bearer-token", 42 | "certbot_email": "test@example.com", 43 | "auto_renew": False, 44 | "renewal_threshold_days": 30 45 | } 46 | 47 | settings_path = os.path.join(flask_app.config['DATA_DIR'], 'settings.json') 48 | with open(settings_path, 'w') as f: 49 | json.dump(test_settings, f) 50 | 51 | client = flask_app.test_client() 52 | 53 | # Test data 54 | test_data = { 55 | 'backend': 'local_filesystem', 56 | 'config': { 57 | 'cert_dir': 'certificates' 58 | } 59 | } 60 | 61 | headers = { 62 | 'Authorization': 'Bearer test-api-bearer-token', 63 | 'Content-Type': 'application/json' 64 | } 65 | 66 | # Test the endpoint 67 | response = client.post( 68 | '/api/storage/test', 69 | headers=headers, 70 | data=json.dumps(test_data) 71 | ) 72 | 73 | print(f"Status Code: {response.status_code}") 74 | print(f"Response Headers: {dict(response.headers)}") 75 | print(f"Response Data: {response.get_data(as_text=True)}") 76 | 77 | if response.status_code == 200: 78 | try: 79 | response_json = response.get_json() 80 | print(f"Parsed JSON: {json.dumps(response_json, indent=2)}") 81 | except Exception as e: 82 | print(f"Error parsing JSON: {e}") 83 | 84 | print("\nTesting Azure KeyVault (should fail due to invalid config):") 85 | 86 | test_data_azure = { 87 | 'backend': 'azure_keyvault', 88 | 'config': { 89 | 'vault_url': 'https://invalid.vault.azure.net/', 90 | 'tenant_id': 'test-tenant', 91 | 'client_id': 'test-client', 92 | 'client_secret': 'test-secret' 93 | } 94 | } 95 | 96 | response2 = client.post( 97 | '/api/storage/test', 98 | headers=headers, 99 | data=json.dumps(test_data_azure) 100 | ) 101 | 102 | print(f"Status Code: {response2.status_code}") 103 | print(f"Response Data: {response2.get_data(as_text=True)}") 104 | 105 | if response2.status_code == 200: 106 | try: 107 | response_json = response2.get_json() 108 | print(f"Parsed JSON: {json.dumps(response_json, indent=2)}") 109 | except Exception as e: 110 | print(f"Error parsing JSON: {e}") 111 | 112 | if __name__ == "__main__": 113 | test_storage_backend_endpoint() 114 | -------------------------------------------------------------------------------- /modules/core/shell.py: -------------------------------------------------------------------------------- 1 | """ 2 | Shell Execution Module for CertMate 3 | Provides an interface for executing shell commands, enabling easier testing and mocking. 4 | """ 5 | 6 | import subprocess 7 | import logging 8 | from typing import List, Optional, Union, Dict, Any 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | class ShellExecutor: 13 | """Interface for executing shell commands""" 14 | 15 | def run(self, cmd: List[str], check: bool = False, capture_output: bool = True, 16 | text: bool = True, timeout: Optional[int] = None, **kwargs) -> subprocess.CompletedProcess: 17 | """ 18 | Run a shell command. 19 | 20 | Args: 21 | cmd: List of command arguments 22 | check: Whether to raise CalledProcessError if return code is non-zero 23 | capture_output: Whether to capture stdout/stderr 24 | text: Whether to decode output as text 25 | timeout: Timeout in seconds 26 | **kwargs: Additional arguments passed to subprocess.run 27 | 28 | Returns: 29 | subprocess.CompletedProcess instance 30 | """ 31 | try: 32 | logger.debug(f"Executing command: {' '.join(cmd)}") 33 | return subprocess.run( 34 | cmd, 35 | check=check, 36 | capture_output=capture_output, 37 | text=text, 38 | timeout=timeout, 39 | **kwargs 40 | ) 41 | except subprocess.TimeoutExpired as e: 42 | logger.error(f"Command timed out: {' '.join(cmd)}") 43 | raise 44 | except Exception as e: 45 | logger.error(f"Command execution failed: {e}") 46 | raise 47 | 48 | class MockShellExecutor(ShellExecutor): 49 | """Mock implementation for testing""" 50 | 51 | def __init__(self): 52 | self.commands_executed = [] 53 | self.responses = {} # Map cmd_substring -> (returncode, stdout, stderr) 54 | self.response_queue = [] # Queue of responses for sequential calls 55 | self.call_count = 0 56 | 57 | def add_response(self, cmd_substring: str, returncode: int = 0, stdout: str = "", stderr: str = ""): 58 | """Add a canned response for a command containing the substring""" 59 | self.responses[cmd_substring] = (returncode, stdout, stderr) 60 | 61 | def set_next_result(self, returncode: int = 0, stdout: str = "", stderr: str = "", should_timeout: bool = False): 62 | """Queue a response for the next command execution""" 63 | self.response_queue.append({ 64 | 'returncode': returncode, 65 | 'stdout': stdout, 66 | 'stderr': stderr, 67 | 'should_timeout': should_timeout 68 | }) 69 | 70 | def run(self, cmd: List[str], **kwargs) -> subprocess.CompletedProcess: 71 | cmd_str = " ".join(cmd) 72 | self.commands_executed.append(cmd_str) 73 | self.call_count += 1 74 | logger.info(f"Mock Executing: {cmd_str}") 75 | 76 | # Check for queued response first 77 | if self.response_queue: 78 | response = self.response_queue.pop(0) 79 | if response['should_timeout']: 80 | raise subprocess.TimeoutExpired(cmd, kwargs.get('timeout', 0)) 81 | return subprocess.CompletedProcess( 82 | cmd, 83 | response['returncode'], 84 | response['stdout'], 85 | response['stderr'] 86 | ) 87 | 88 | # Look for canned response by substring 89 | for k, v in self.responses.items(): 90 | if k in cmd_str: 91 | returncode, stdout, stderr = v 92 | return subprocess.CompletedProcess(cmd, returncode, stdout, stderr) 93 | 94 | # Default success 95 | return subprocess.CompletedProcess(cmd, 0, "", "") 96 | -------------------------------------------------------------------------------- /test_certificate_listing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test script to verify certificate listing functionality 4 | """ 5 | 6 | import os 7 | import sys 8 | import tempfile 9 | import shutil 10 | from pathlib import Path 11 | 12 | # Add the current directory to Python path 13 | sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) 14 | 15 | def test_certificate_listing(): 16 | """Test that certificates are properly listed after creation""" 17 | 18 | # Import the functions we need to test 19 | from app import get_certificate_info, load_settings, CERT_DIR 20 | 21 | print("Testing certificate listing functionality...") 22 | 23 | # Create a test certificate directory structure 24 | test_domain = "test.example.com" 25 | test_cert_dir = CERT_DIR / test_domain 26 | test_cert_dir.mkdir(parents=True, exist_ok=True) 27 | 28 | # Create a dummy certificate file 29 | cert_file = test_cert_dir / "cert.pem" 30 | with open(cert_file, 'w') as f: 31 | f.write("-----BEGIN CERTIFICATE-----\nDUMMY CERTIFICATE FOR TESTING\n-----END CERTIFICATE-----\n") 32 | 33 | # Test get_certificate_info function 34 | print(f"Testing get_certificate_info for {test_domain}...") 35 | cert_info = get_certificate_info(test_domain) 36 | 37 | if cert_info: 38 | print(f"✓ Certificate info found: {cert_info}") 39 | if cert_info.get('domain') == test_domain: 40 | print("✓ Domain matches") 41 | else: 42 | print(f"✗ Domain mismatch: expected {test_domain}, got {cert_info.get('domain')}") 43 | else: 44 | print("✗ No certificate info returned") 45 | 46 | # Test certificate listing from API endpoint logic 47 | print("\nTesting certificate listing logic...") 48 | 49 | # We can't directly test the API method without proper Flask context, 50 | # but we can test the core logic 51 | settings = load_settings() 52 | certificates = [] 53 | 54 | # Get all domains from settings 55 | domains_from_settings = settings.get('domains', []) 56 | 57 | # Also check for certificates that exist on disk but might not be in settings 58 | cert_dirs = [] 59 | if CERT_DIR.exists(): 60 | cert_dirs = [d for d in CERT_DIR.iterdir() if d.is_dir()] 61 | 62 | # Create a set of all domains to check (from settings and disk) 63 | all_domains = set() 64 | 65 | # Add domains from settings 66 | for domain_config in domains_from_settings: 67 | domain_name = domain_config.get('domain') if isinstance(domain_config, dict) else domain_config 68 | if domain_name: 69 | all_domains.add(domain_name) 70 | 71 | # Add domains from disk 72 | for cert_dir in cert_dirs: 73 | all_domains.add(cert_dir.name) 74 | 75 | print(f"Found domains: {all_domains}") 76 | 77 | # Get certificate info for all domains 78 | for domain_name in all_domains: 79 | if domain_name: 80 | cert_info = get_certificate_info(domain_name) 81 | if cert_info: 82 | certificates.append(cert_info) 83 | 84 | print(f"Found {len(certificates)} certificates") 85 | 86 | # Check if our test domain is in the list 87 | test_domain_found = False 88 | for cert in certificates: 89 | if cert.get('domain') == test_domain: 90 | test_domain_found = True 91 | break 92 | 93 | if test_domain_found: 94 | print(f"✓ Test domain {test_domain} found in certificate list") 95 | else: 96 | print(f"✗ Test domain {test_domain} not found in certificate list") 97 | 98 | # Clean up 99 | print("\nCleaning up test files...") 100 | shutil.rmtree(test_cert_dir, ignore_errors=True) 101 | 102 | print("Test completed!") 103 | 104 | if __name__ == '__main__': 105 | test_certificate_listing() 106 | -------------------------------------------------------------------------------- /modules/core/auth.py: -------------------------------------------------------------------------------- 1 | """ 2 | Authentication module for CertMate 3 | Handles authentication decorators and security functions 4 | """ 5 | 6 | import logging 7 | import secrets 8 | from functools import wraps 9 | from flask import request, jsonify 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class AuthManager: 15 | """Class to handle authentication and authorization""" 16 | 17 | def __init__(self, settings_manager): 18 | self.settings_manager = settings_manager 19 | 20 | def require_auth(self, f): 21 | """Enhanced decorator to require bearer token authentication""" 22 | @wraps(f) 23 | def decorated_function(*args, **kwargs): 24 | try: 25 | # Get bearer token from Authorization header 26 | auth_header = request.headers.get('Authorization') 27 | if not auth_header: 28 | return {'error': 'Authorization header required', 'code': 'AUTH_HEADER_MISSING'}, 401 29 | 30 | try: 31 | scheme, token = auth_header.split(' ', 1) 32 | if scheme.lower() != 'bearer': 33 | return {'error': 'Invalid authorization scheme. Use Bearer token', 'code': 'INVALID_AUTH_SCHEME'}, 401 34 | if not token.strip(): 35 | return {'error': 'Invalid authorization header format. Use: Bearer ', 'code': 'INVALID_AUTH_FORMAT'}, 401 36 | except ValueError: 37 | return {'error': 'Invalid authorization header format. Use: Bearer ', 'code': 'INVALID_AUTH_FORMAT'}, 401 38 | 39 | # Load current settings to get the valid token 40 | settings = self.settings_manager.load_settings() 41 | expected_token = settings.get('api_bearer_token') 42 | 43 | if not expected_token: 44 | return {'error': 'Server configuration error: no API token configured', 'code': 'SERVER_CONFIG_ERROR'}, 500 45 | 46 | # Validate token strength using imported function 47 | from modules.core.utils import validate_api_token 48 | is_valid, token_or_error = validate_api_token(expected_token) 49 | if not is_valid: 50 | logger.error(f"Server has weak API token: {token_or_error}") 51 | return {'error': 'Server security configuration error', 'code': 'WEAK_SERVER_TOKEN'}, 500 52 | 53 | # Use constant-time comparison to prevent timing attacks 54 | if not secrets.compare_digest(token, expected_token): 55 | logger.warning(f"Invalid API token attempt from {request.remote_addr}") 56 | return {'error': 'Invalid or expired token', 'code': 'INVALID_TOKEN'}, 401 57 | 58 | return f(*args, **kwargs) 59 | except Exception as e: 60 | logger.error(f"Authentication error: {e}") 61 | return {'error': 'Authentication failed', 'code': 'AUTH_ERROR'}, 401 62 | 63 | return decorated_function 64 | 65 | def validate_api_token(self, token): 66 | """Validate API token against current settings""" 67 | try: 68 | settings = self.settings_manager.load_settings() 69 | valid_token = settings.get('api_bearer_token') 70 | return token == valid_token if valid_token else False 71 | except Exception as e: 72 | logger.error(f"Error validating API token: {e}") 73 | return False 74 | 75 | def get_current_token(self): 76 | """Get the current API bearer token from settings""" 77 | try: 78 | settings = self.settings_manager.load_settings() 79 | return settings.get('api_bearer_token') 80 | except Exception as e: 81 | logger.error(f"Error getting current token: {e}") 82 | return None 83 | -------------------------------------------------------------------------------- /debug_storage_simple.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Simple test script to debug storage backend test connection 4 | """ 5 | 6 | import sys 7 | import os 8 | import pytest 9 | sys.path.insert(0, os.path.dirname(__file__)) 10 | 11 | def test_storage_backend_manually(): 12 | """Test the storage backend with pytest style setup""" 13 | 14 | # Use pytest to set up the environment like the tests do 15 | from tests.conftest import app 16 | 17 | # This will give us an app with proper test setup 18 | flask_app = None 19 | for test_app in app(): 20 | flask_app = test_app 21 | break 22 | 23 | if not flask_app: 24 | print("Failed to create test app") 25 | return 26 | 27 | import json 28 | 29 | client = flask_app.test_client() 30 | 31 | # Test data 32 | test_data = { 33 | 'backend': 'local_filesystem', 34 | 'config': { 35 | 'cert_dir': 'certificates' 36 | } 37 | } 38 | 39 | headers = { 40 | 'Authorization': 'Bearer test-api-bearer-token', 41 | 'Content-Type': 'application/json' 42 | } 43 | 44 | print("Testing storage backend endpoint with proper test setup...") 45 | 46 | # Test the endpoint 47 | response = client.post( 48 | '/api/storage/test', 49 | headers=headers, 50 | data=json.dumps(test_data) 51 | ) 52 | 53 | print(f"Status Code: {response.status_code}") 54 | print(f"Response Headers: {dict(response.headers)}") 55 | print(f"Response Data: {response.get_data(as_text=True)}") 56 | 57 | if response.status_code == 200: 58 | try: 59 | response_json = response.get_json() 60 | print(f"Parsed JSON: {json.dumps(response_json, indent=2)}") 61 | 62 | # Check the expected format 63 | if 'success' in response_json: 64 | print(f"✅ Success field present: {response_json['success']}") 65 | if 'message' in response_json: 66 | print(f"✅ Message field present: {response_json['message']}") 67 | if 'backend' in response_json: 68 | print(f"✅ Backend field present: {response_json['backend']}") 69 | 70 | except Exception as e: 71 | print(f"Error parsing JSON: {e}") 72 | else: 73 | print(f"❌ Request failed with status {response.status_code}") 74 | 75 | print("\n" + "="*50) 76 | print("Testing Azure KeyVault (should fail gracefully):") 77 | 78 | test_data_azure = { 79 | 'backend': 'azure_keyvault', 80 | 'config': { 81 | 'vault_url': 'https://invalid.vault.azure.net/', 82 | 'tenant_id': 'test-tenant', 83 | 'client_id': 'test-client', 84 | 'client_secret': 'test-secret' 85 | } 86 | } 87 | 88 | response2 = client.post( 89 | '/api/storage/test', 90 | headers=headers, 91 | data=json.dumps(test_data_azure) 92 | ) 93 | 94 | print(f"Status Code: {response2.status_code}") 95 | print(f"Response Data: {response2.get_data(as_text=True)}") 96 | 97 | if response2.status_code == 200: 98 | try: 99 | response_json = response2.get_json() 100 | print(f"Parsed JSON: {json.dumps(response_json, indent=2)}") 101 | 102 | # Check the expected format 103 | if 'success' in response_json: 104 | print(f"✅ Success field present: {response_json['success']}") 105 | if not response_json['success']: 106 | print("✅ Correctly returned success=false for invalid config") 107 | if 'message' in response_json: 108 | print(f"✅ Message field present: {response_json['message']}") 109 | 110 | except Exception as e: 111 | print(f"Error parsing JSON: {e}") 112 | 113 | if __name__ == "__main__": 114 | test_storage_backend_manually() 115 | -------------------------------------------------------------------------------- /.github/workflows/docker-multiplatform.yml.disabled: -------------------------------------------------------------------------------- 1 | name: Build Multi-Platform Docker Images 2 | 3 | on: 4 | push: 5 | branches: [ main, develop ] 6 | tags: [ 'v*' ] 7 | pull_request: 8 | branches: [ main ] 9 | workflow_dispatch: 10 | inputs: 11 | platforms: 12 | description: 'Target platforms (comma-separated)' 13 | required: false 14 | default: 'linux/amd64,linux/arm64' 15 | type: string 16 | push_to_registry: 17 | description: 'Push to Docker Hub' 18 | required: false 19 | default: true 20 | type: boolean 21 | 22 | env: 23 | REGISTRY: docker.io 24 | IMAGE_NAME: certmate 25 | 26 | jobs: 27 | build: 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - name: Checkout repository 32 | uses: actions/checkout@v4 33 | 34 | - name: Set up Docker Buildx 35 | uses: docker/setup-buildx-action@v3 36 | with: 37 | version: latest 38 | 39 | - name: Log in to Docker Hub 40 | if: github.event_name != 'pull_request' 41 | uses: docker/login-action@v3 42 | with: 43 | registry: ${{ env.REGISTRY }} 44 | username: ${{ secrets.DOCKERHUB_USERNAME }} 45 | password: ${{ secrets.DOCKERHUB_TOKEN }} 46 | 47 | - name: Extract metadata 48 | id: meta 49 | uses: docker/metadata-action@v5 50 | with: 51 | images: ${{ env.REGISTRY }}/${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }} 52 | tags: | 53 | type=ref,event=branch 54 | type=ref,event=pr 55 | type=semver,pattern={{version}} 56 | type=semver,pattern={{major}}.{{minor}} 57 | type=semver,pattern={{major}} 58 | type=raw,value=latest,enable={{is_default_branch}} 59 | 60 | - name: Determine platforms 61 | id: platforms 62 | run: | 63 | if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then 64 | echo "platforms=${{ github.event.inputs.platforms }}" >> $GITHUB_OUTPUT 65 | else 66 | echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT 67 | fi 68 | 69 | - name: Determine push setting 70 | id: should_push 71 | run: | 72 | if [ "${{ github.event_name }}" = "pull_request" ]; then 73 | echo "push=false" >> $GITHUB_OUTPUT 74 | elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then 75 | echo "push=${{ github.event.inputs.push_to_registry }}" >> $GITHUB_OUTPUT 76 | else 77 | echo "push=true" >> $GITHUB_OUTPUT 78 | fi 79 | 80 | - name: Build and push Docker image 81 | uses: docker/build-push-action@v5 82 | with: 83 | context: . 84 | platforms: ${{ steps.platforms.outputs.platforms }} 85 | push: ${{ steps.should_push.outputs.push }} 86 | tags: ${{ steps.meta.outputs.tags }} 87 | labels: ${{ steps.meta.outputs.labels }} 88 | cache-from: type=gha 89 | cache-to: type=gha,mode=max 90 | build-args: | 91 | REQUIREMENTS_FILE=requirements-minimal.txt 92 | 93 | - name: Update Docker Hub description 94 | if: github.ref == 'refs/heads/main' && steps.should_push.outputs.push == 'true' 95 | uses: peter-evans/dockerhub-description@v3 96 | with: 97 | username: ${{ secrets.DOCKERHUB_USERNAME }} 98 | password: ${{ secrets.DOCKERHUB_TOKEN }} 99 | repository: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }} 100 | readme-filepath: ./README.md 101 | 102 | - name: Image digest 103 | if: steps.should_push.outputs.push == 'true' 104 | run: echo ${{ steps.build.outputs.digest }} 105 | 106 | security-scan: 107 | runs-on: ubuntu-latest 108 | needs: build 109 | if: github.event_name != 'pull_request' 110 | 111 | steps: 112 | - name: Run Trivy vulnerability scanner 113 | uses: aquasecurity/trivy-action@master 114 | with: 115 | image-ref: ${{ env.REGISTRY }}/${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:latest 116 | format: 'sarif' 117 | output: 'trivy-results.sarif' 118 | 119 | - name: Upload Trivy scan results to GitHub Security tab 120 | uses: github/codeql-action/upload-sarif@v2 121 | if: always() 122 | with: 123 | sarif_file: 'trivy-results.sarif' 124 | -------------------------------------------------------------------------------- /test_dns_provider_inheritance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test script to verify DNS provider inheritance and smart suggestions 4 | """ 5 | 6 | import os 7 | import sys 8 | import json 9 | from pathlib import Path 10 | 11 | # Add the current directory to Python path 12 | sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) 13 | 14 | def test_dns_provider_inheritance(): 15 | """Test DNS provider inheritance and smart suggestions""" 16 | 17 | # Import the functions we need to test 18 | from app import get_domain_dns_provider, suggest_dns_provider_for_domain 19 | 20 | print("Testing DNS provider inheritance and smart suggestions...") 21 | 22 | # Test settings similar to your current configuration 23 | test_settings = { 24 | "domains": [ 25 | { 26 | "domain": "test2.audiolibri.org", 27 | "dns_provider": "cloudflare", 28 | "account_id": "default" 29 | }, 30 | { 31 | "domain": "aws-test3.test.certmate.org", 32 | "dns_provider": "route53", 33 | "account_id": "certmate_test" 34 | }, 35 | { 36 | "domain": "cf-test1.audiolibri.org", 37 | "dns_provider": "cloudflare", 38 | "account_id": "default" 39 | } 40 | ], 41 | "dns_provider": "route53", 42 | "default_accounts": { 43 | "cloudflare": "default", 44 | "route53": "certmate_test" 45 | } 46 | } 47 | 48 | # Test existing domain DNS provider detection 49 | print("\n1. Testing existing domain DNS provider detection:") 50 | test_cases = [ 51 | ("test2.audiolibri.org", "cloudflare"), 52 | ("aws-test3.test.certmate.org", "route53"), 53 | ("cf-test1.audiolibri.org", "cloudflare"), 54 | ("unknown-domain.com", "route53") # Should fall back to global default 55 | ] 56 | 57 | for domain, expected_provider in test_cases: 58 | detected_provider = get_domain_dns_provider(domain, test_settings) 59 | status = "✓" if detected_provider == expected_provider else "✗" 60 | print(f" {status} {domain} -> {detected_provider} (expected: {expected_provider})") 61 | 62 | # Test smart DNS provider suggestions 63 | print("\n2. Testing smart DNS provider suggestions:") 64 | suggestion_test_cases = [ 65 | ("new-cf-test.audiolibri.org", "cloudflare"), 66 | ("aws-new-test.test.certmate.org", "route53"), 67 | ("cf-something.example.com", "cloudflare"), 68 | ("random-domain.com", "route53") # Should fall back to global default 69 | ] 70 | 71 | for domain, expected_provider in suggestion_test_cases: 72 | suggested_provider, confidence = suggest_dns_provider_for_domain(domain, test_settings) 73 | status = "✓" if suggested_provider == expected_provider else "✗" 74 | print(f" {status} {domain} -> {suggested_provider} (confidence: {confidence}%, expected: {expected_provider})") 75 | 76 | print("\n3. Testing certificate creation logic simulation:") 77 | 78 | # Simulate the certificate creation DNS provider selection logic 79 | def simulate_dns_provider_selection(domain, provided_dns_provider, settings): 80 | """Simulate the DNS provider selection logic from certificate creation""" 81 | if provided_dns_provider: 82 | return provided_dns_provider 83 | 84 | # Check existing domain configuration 85 | existing_provider = get_domain_dns_provider(domain, settings) 86 | if existing_provider and existing_provider != settings.get('dns_provider', 'cloudflare'): 87 | return existing_provider 88 | 89 | # Use smart suggestion 90 | suggested_provider, confidence = suggest_dns_provider_for_domain(domain, settings) 91 | if confidence >= 70: 92 | return suggested_provider 93 | 94 | # Fall back to global default 95 | return settings.get('dns_provider', 'cloudflare') 96 | 97 | creation_test_cases = [ 98 | ("test2.audiolibri.org", None, "cloudflare"), # Existing domain 99 | ("new-cf-domain.audiolibri.org", None, "cloudflare"), # Smart suggestion 100 | ("aws-new-domain.test.certmate.org", None, "route53"), # Smart suggestion 101 | ("random-new-domain.com", None, "route53"), # Global default 102 | ("any-domain.com", "digitalocean", "digitalocean"), # Explicit provider 103 | ] 104 | 105 | for domain, provided_provider, expected_provider in creation_test_cases: 106 | selected_provider = simulate_dns_provider_selection(domain, provided_provider, test_settings) 107 | status = "✓" if selected_provider == expected_provider else "✗" 108 | print(f" {status} {domain} (provided: {provided_provider}) -> {selected_provider} (expected: {expected_provider})") 109 | 110 | print("\nTest completed!") 111 | 112 | if __name__ == '__main__': 113 | test_dns_provider_inheritance() 114 | -------------------------------------------------------------------------------- /debug_response.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test script to see the actual response from storage backend test 4 | """ 5 | 6 | import pytest 7 | import json 8 | import sys 9 | import os 10 | 11 | # Add the parent directory to the path 12 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) 13 | 14 | def test_storage_response(): 15 | """Test to see the actual storage backend response""" 16 | 17 | # Create a minimal test like in test_storage_backend_api.py 18 | import tempfile 19 | import shutil 20 | from pathlib import Path 21 | 22 | # Set up test environment like conftest.py does 23 | test_dir = tempfile.mkdtemp() 24 | 25 | os.environ['TESTING'] = 'True' 26 | os.environ['FLASK_ENV'] = 'testing' 27 | os.environ['SECRET_KEY'] = 'test-secret-key-12345' 28 | 29 | # Import the app 30 | from app import app as flask_app 31 | 32 | # Configure app for testing 33 | flask_app.config.update({ 34 | 'TESTING': True, 35 | 'WTF_CSRF_ENABLED': False, 36 | 'SECRET_KEY': 'test-secret-key-12345', 37 | 'CERT_DIR': os.path.join(test_dir, 'certificates'), 38 | 'DATA_DIR': os.path.join(test_dir, 'data'), 39 | }) 40 | 41 | # Create test directories 42 | os.makedirs(flask_app.config['CERT_DIR'], exist_ok=True) 43 | os.makedirs(flask_app.config['DATA_DIR'], exist_ok=True) 44 | 45 | # Create test settings file 46 | test_settings = { 47 | "cloudflare_api_token": "test-token", 48 | "cloudflare_zone_id": "test-zone-id", 49 | "cloudflare_email": "test@example.com", 50 | "certbot_email": "test@example.com", 51 | "auto_renew": False, 52 | "renewal_threshold_days": 30, 53 | "api_bearer_token": "test-api-bearer-token" 54 | } 55 | 56 | settings_path = os.path.join(flask_app.config['DATA_DIR'], 'settings.json') 57 | with open(settings_path, 'w') as f: 58 | json.dump(test_settings, f) 59 | 60 | # Create test client 61 | client = flask_app.test_client() 62 | 63 | # Set up headers 64 | auth_headers = { 65 | 'Authorization': 'Bearer test-api-bearer-token', 66 | 'Content-Type': 'application/json' 67 | } 68 | 69 | print("Testing local filesystem backend...") 70 | 71 | # Test data for local filesystem 72 | test_data = { 73 | 'backend': 'local_filesystem', 74 | 'config': { 75 | 'cert_dir': 'test_certificates' 76 | } 77 | } 78 | 79 | # Make the request 80 | response = client.post( 81 | '/api/storage/test', 82 | headers=auth_headers, 83 | data=json.dumps(test_data) 84 | ) 85 | 86 | print(f"Status Code: {response.status_code}") 87 | print(f"Headers: {dict(response.headers)}") 88 | print(f"Raw Response: {response.get_data(as_text=True)}") 89 | 90 | if response.status_code == 200: 91 | try: 92 | response_json = response.get_json() 93 | print(f"JSON Response: {json.dumps(response_json, indent=2)}") 94 | 95 | # Check response structure for UI 96 | required_fields = ['success', 'message', 'backend'] 97 | for field in required_fields: 98 | if field in response_json: 99 | print(f"✅ {field}: {response_json[field]}") 100 | else: 101 | print(f"❌ Missing field: {field}") 102 | 103 | except Exception as e: 104 | print(f"Error parsing JSON: {e}") 105 | else: 106 | print(f"❌ Request failed") 107 | 108 | # Test Azure KeyVault (should fail) 109 | print("\n" + "="*50) 110 | print("Testing Azure KeyVault backend (invalid config)...") 111 | 112 | test_data_azure = { 113 | 'backend': 'azure_keyvault', 114 | 'config': { 115 | 'vault_url': 'https://invalid.vault.azure.net/', 116 | 'tenant_id': 'test-tenant', 117 | 'client_id': 'test-client', 118 | 'client_secret': 'test-secret' 119 | } 120 | } 121 | 122 | response2 = client.post( 123 | '/api/storage/test', 124 | headers=auth_headers, 125 | data=json.dumps(test_data_azure) 126 | ) 127 | 128 | print(f"Status Code: {response2.status_code}") 129 | print(f"Raw Response: {response2.get_data(as_text=True)}") 130 | 131 | if response2.status_code == 200: 132 | try: 133 | response_json = response2.get_json() 134 | print(f"JSON Response: {json.dumps(response_json, indent=2)}") 135 | 136 | # Check response structure for UI 137 | required_fields = ['success', 'message', 'backend'] 138 | for field in required_fields: 139 | if field in response_json: 140 | print(f"✅ {field}: {response_json[field]}") 141 | else: 142 | print(f"❌ Missing field: {field}") 143 | 144 | except Exception as e: 145 | print(f"Error parsing JSON: {e}") 146 | 147 | # Cleanup 148 | try: 149 | shutil.rmtree(test_dir, ignore_errors=True) 150 | except: 151 | pass 152 | 153 | if __name__ == "__main__": 154 | test_storage_response() 155 | -------------------------------------------------------------------------------- /modules/core/rate_limit.py: -------------------------------------------------------------------------------- 1 | """ 2 | Rate limiting configuration for CertMate 3 | Simple rate limiting configuration for API endpoints 4 | """ 5 | 6 | import logging 7 | from typing import Dict, Optional 8 | from functools import wraps 9 | from flask import request 10 | from collections import defaultdict 11 | from time import time 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class RateLimitConfig: 17 | """Configuration for API rate limiting.""" 18 | 19 | # Default rate limits (requests per minute) 20 | DEFAULT_LIMITS = { 21 | 'default': 100, # 100 requests/minute default 22 | 'certificate_create': 30, # Creating certs is expensive 23 | 'certificate_batch': 10, # Batch operations are very expensive 24 | 'certificate_list': 60, # Listing is cheaper 25 | 'certificate_revoke': 60, 26 | 'certificate_renew': 30, 27 | 'ocsp_status': 200, # OCSP should be high 28 | 'crl_download': 60, 29 | } 30 | 31 | def __init__(self, custom_limits: Optional[Dict[str, int]] = None): 32 | """ 33 | Initialize Rate Limit Config. 34 | 35 | Args: 36 | custom_limits: Custom rate limit overrides 37 | """ 38 | self.limits = dict(self.DEFAULT_LIMITS) 39 | if custom_limits: 40 | self.limits.update(custom_limits) 41 | 42 | logger.info(f"Rate limiting configured with {len(self.limits)} endpoint limits") 43 | 44 | def get_limit(self, endpoint: str) -> int: 45 | """ 46 | Get rate limit for an endpoint. 47 | 48 | Args: 49 | endpoint: Endpoint name or path 50 | 51 | Returns: 52 | Rate limit (requests per minute) 53 | """ 54 | # Try exact match first 55 | if endpoint in self.limits: 56 | return self.limits[endpoint] 57 | 58 | # Try prefix match 59 | for limit_key in self.limits: 60 | if endpoint.startswith(limit_key): 61 | return self.limits[limit_key] 62 | 63 | # Return default 64 | return self.limits.get('default', 100) 65 | 66 | 67 | class SimpleRateLimiter: 68 | """Simple in-memory rate limiter (perfect for single-instance apps).""" 69 | 70 | def __init__(self, config: RateLimitConfig): 71 | """ 72 | Initialize Rate Limiter. 73 | 74 | Args: 75 | config: RateLimitConfig instance 76 | """ 77 | self.config = config 78 | self.requests = defaultdict(list) # Track request times per IP 79 | 80 | def is_allowed(self, identifier: str, endpoint: str) -> bool: 81 | """ 82 | Check if request is allowed under rate limit. 83 | 84 | Args: 85 | identifier: IP address or user identifier 86 | endpoint: Endpoint being accessed 87 | 88 | Returns: 89 | True if request is allowed, False if rate limited 90 | """ 91 | limit = self.config.get_limit(endpoint) 92 | current_time = time() 93 | window_start = current_time - 60 # 1 minute window 94 | 95 | # Get requests for this identifier 96 | key = f"{identifier}:{endpoint}" 97 | self.requests[key] = [ 98 | req_time for req_time in self.requests[key] 99 | if req_time > window_start 100 | ] 101 | 102 | # Check if under limit 103 | if len(self.requests[key]) >= limit: 104 | logger.warning(f"Rate limit exceeded for {identifier} on {endpoint}") 105 | return False 106 | 107 | # Record this request 108 | self.requests[key].append(current_time) 109 | return True 110 | 111 | def cleanup_old_entries(self) -> None: 112 | """Clean up old request records (call periodically).""" 113 | try: 114 | current_time = time() 115 | window_start = current_time - 3600 # Keep 1 hour of data 116 | 117 | for key in list(self.requests.keys()): 118 | self.requests[key] = [ 119 | req_time for req_time in self.requests[key] 120 | if req_time > window_start 121 | ] 122 | if not self.requests[key]: 123 | del self.requests[key] 124 | 125 | except Exception as e: 126 | logger.error(f"Error cleaning up rate limit entries: {e}") 127 | 128 | 129 | def rate_limit_decorator(limiter: SimpleRateLimiter, endpoint: str): 130 | """ 131 | Decorator for rate limiting Flask endpoints. 132 | 133 | Args: 134 | limiter: SimpleRateLimiter instance 135 | endpoint: Endpoint identifier 136 | 137 | Returns: 138 | Decorator function 139 | """ 140 | def decorator(f): 141 | @wraps(f) 142 | def decorated_function(*args, **kwargs): 143 | # Get client IP (prefer X-Forwarded-For for proxied requests) 144 | client_ip = request.headers.get( 145 | 'X-Forwarded-For', 146 | request.remote_addr 147 | ).split(',')[0].strip() 148 | 149 | # Check rate limit 150 | if not limiter.is_allowed(client_ip, endpoint): 151 | logger.warning( 152 | f"Rate limit exceeded: {client_ip} on {endpoint}" 153 | ) 154 | return { 155 | 'error': 'Rate limit exceeded', 156 | 'message': 'Too many requests. Please try again later.', 157 | 'retry_after': 60 158 | }, 429 159 | 160 | return f(*args, **kwargs) 161 | 162 | return decorated_function 163 | 164 | return decorator 165 | -------------------------------------------------------------------------------- /MODULAR_STRUCTURE.md: -------------------------------------------------------------------------------- 1 | # CertMate Modular Architecture 2 | 3 | ## 📁 Directory Structure 4 | 5 | ``` 6 | certmate/ 7 | ├── app.py # Main application entry point (modular + compatibility layer) 8 | ├── app.py.notmodular # Original monolithic backup 9 | ├── modules/ 10 | │ ├── core/ # 🔧 Core business logic 11 | │ │ ├── __init__.py # Core module exports 12 | │ │ ├── utils.py # Utility functions (validation, config creation, tokens) 13 | │ │ ├── metrics.py # Prometheus metrics and monitoring 14 | │ │ ├── auth.py # Authentication decorators and management 15 | │ │ ├── cache.py # Deployment status caching 16 | │ │ ├── certificates.py # Certificate operations (create, renew, info) 17 | │ │ ├── dns_providers.py # DNS provider management 18 | │ │ ├── file_operations.py # Safe file I/O and backup management 19 | │ │ └── settings.py # Settings management and migrations 20 | │ ├── api/ # 🌐 REST API layer 21 | │ │ ├── __init__.py # API module exports 22 | │ │ ├── models.py # Flask-RESTX API models/schemas 23 | │ │ └── resources.py # API endpoint implementations 24 | │ └── web/ # 🖥️ Web interface layer 25 | │ ├── __init__.py # Web module exports 26 | │ └── routes.py # Web interface routes and templates 27 | ├── tests/ # Test suite 28 | └── templates/ # HTML templates 29 | ``` 30 | 31 | ## 🎯 Design Principles 32 | 33 | ### 1. **Separation of Concerns** 34 | - **Core**: Business logic and data management 35 | - **API**: REST endpoints and request/response handling 36 | - **Web**: HTML interface and user interactions 37 | 38 | ### 2. **Modular Dependencies** 39 | ``` 40 | Web ──┐ 41 | ├──► Core (auth, certificates, settings, etc.) 42 | API ───┘ 43 | 44 | Core ──► Utils & Metrics (shared utilities) 45 | ``` 46 | 47 | ### 3. **Clean Imports** 48 | ```python 49 | # Core utilities 50 | from modules.core.utils import validate_email, generate_secure_token 51 | from modules.core.metrics import metrics_collector 52 | 53 | # Business logic managers 54 | from modules.core import ( 55 | AuthManager, CertificateManager, SettingsManager, 56 | DNSManager, CacheManager, FileOperations 57 | ) 58 | 59 | # API and web layers 60 | from modules.api import create_api_models, create_api_resources 61 | from modules.web import register_web_routes 62 | ``` 63 | 64 | ## ✅ Benefits Achieved 65 | 66 | ### **1. Maintainability** 67 | - Each module has a single responsibility 68 | - Clear boundaries between layers 69 | - Easy to locate and modify specific functionality 70 | 71 | ### **2. Testability** 72 | - Individual modules can be tested in isolation 73 | - Mock dependencies at module boundaries 74 | - Clear interfaces for testing 75 | 76 | ### **3. Scalability** 77 | - New features can be added to appropriate modules 78 | - API and web interfaces can evolve independently 79 | - Core logic remains stable 80 | 81 | ### **4. Reusability** 82 | - Core managers can be used by different interfaces 83 | - Utilities are shared across modules 84 | - Business logic is decoupled from presentation 85 | 86 | ## 🔄 Migration Status 87 | 88 | ### ✅ **Completed** 89 | - [x] Modular architecture implementation 90 | - [x] Core business logic separation 91 | - [x] API and web layer separation 92 | - [x] Backward compatibility layer 93 | - [x] Authentication system preservation 94 | - [x] File structure reorganization 95 | - [x] Import path updates 96 | - [x] Server functionality verification 97 | - [x] All API endpoints working 98 | - [x] Web interface functional 99 | - [x] Settings management and restore 100 | - [x] Certificate management with DNS provider tracking 101 | - [x] All legacy functions preserved for test compatibility 102 | 103 | ### ✅ **Additional Features Added** 104 | - [x] Certificate metadata storage (DNS provider tracking) 105 | - [x] Settings backup and restore functionality 106 | - [x] Enhanced DNS provider display accuracy 107 | - [x] Modular metrics and monitoring 108 | - [x] Comprehensive compatibility layer 109 | 110 | ### ⚠️ **Notes** 111 | - Original monolithic `app.py` preserved as `app.py.notmodular` 112 | - All 17+ legacy functions available through compatibility layer 113 | - 53+ functions now available (compared to 41 in original) 114 | - Test compatibility maintained through function exports 115 | - DNS provider display issue resolved with metadata files 116 | 117 | ## 🚀 Usage 118 | 119 | ### **Development Server** 120 | ```bash 121 | python app.py 122 | # or 123 | python -m app 124 | ``` 125 | 126 | ### **WSGI Production** 127 | ```python 128 | from app import app 129 | # Use with gunicorn, uwsgi, etc. 130 | ``` 131 | 132 | ### **Importing Modules** 133 | ```python 134 | # For new code, use modular imports 135 | from modules.core.certificates import CertificateManager 136 | from modules.core.settings import SettingsManager 137 | 138 | # For legacy compatibility 139 | from app import create_certificate, load_settings 140 | ``` 141 | 142 | ## 📝 Notes 143 | 144 | - Original monolithic `app.py` preserved as `app.py.notmodular` 145 | - Full backward compatibility maintained through compatibility layer 146 | - All original functionality preserved and tested 147 | - Server starts successfully and serves all endpoints 148 | - Ready for production use 149 | 150 | --- 151 | 152 | **Status**: ✅ **Modular refactoring complete and fully functional** 153 | **Date**: July 12, 2025 154 | **Features**: All original functionality preserved + enhanced with metadata tracking, backup/restore, and improved DNS provider accuracy 155 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | fabrizio.salmi@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /test_certificate_creation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test script to simulate certificate creation with DNS provider inheritance 4 | """ 5 | 6 | import os 7 | import sys 8 | import json 9 | from pathlib import Path 10 | 11 | # Add the current directory to Python path 12 | sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) 13 | 14 | def simulate_certificate_creation(): 15 | """Simulate the certificate creation process""" 16 | 17 | from app import ( 18 | get_domain_dns_provider, 19 | get_dns_provider_account_config, 20 | suggest_dns_provider_for_domain, 21 | load_settings 22 | ) 23 | 24 | print("Simulating certificate creation process...") 25 | 26 | # Load your actual settings 27 | settings = load_settings() 28 | 29 | # Test scenarios 30 | test_scenarios = [ 31 | # Scenario 1: Creating cert for existing domain (should use configured provider) 32 | { 33 | "domain": "test2.audiolibri.org", 34 | "provided_dns_provider": None, 35 | "provided_account_id": None, 36 | "expected_provider": "cloudflare", 37 | "expected_account": "default" 38 | }, 39 | # Scenario 2: Creating cert for Route53 domain 40 | { 41 | "domain": "aws-test3.test.certmate.org", 42 | "provided_dns_provider": None, 43 | "provided_account_id": None, 44 | "expected_provider": "route53", 45 | "expected_account": "certmate_test" 46 | }, 47 | # Scenario 3: Creating cert for new Cloudflare domain (smart suggestion) 48 | { 49 | "domain": "new-cf-test.audiolibri.org", 50 | "provided_dns_provider": None, 51 | "provided_account_id": None, 52 | "expected_provider": "cloudflare", 53 | "expected_account": "default" 54 | }, 55 | # Scenario 4: Creating cert for new Route53 domain (smart suggestion) 56 | { 57 | "domain": "new-aws-test.test.certmate.org", 58 | "provided_dns_provider": None, 59 | "provided_account_id": None, 60 | "expected_provider": "route53", 61 | "expected_account": "certmate_test" 62 | }, 63 | # Scenario 5: Explicit provider override 64 | { 65 | "domain": "any-domain.com", 66 | "provided_dns_provider": "cloudflare", 67 | "provided_account_id": "default", 68 | "expected_provider": "cloudflare", 69 | "expected_account": "default" 70 | } 71 | ] 72 | 73 | print(f"\nSimulating certificate creation scenarios:") 74 | 75 | for i, scenario in enumerate(test_scenarios, 1): 76 | domain = scenario["domain"] 77 | provided_dns_provider = scenario["provided_dns_provider"] 78 | provided_account_id = scenario["provided_account_id"] 79 | expected_provider = scenario["expected_provider"] 80 | expected_account = scenario["expected_account"] 81 | 82 | print(f"\n{i}. Testing: {domain}") 83 | print(f" Provided DNS provider: {provided_dns_provider or 'None'}") 84 | print(f" Provided account ID: {provided_account_id or 'None'}") 85 | 86 | # Simulate the certificate creation DNS provider selection logic 87 | if provided_dns_provider: 88 | dns_provider = provided_dns_provider 89 | print(f" Using provided DNS provider: {dns_provider}") 90 | else: 91 | # Check existing domain configuration 92 | existing_provider = get_domain_dns_provider(domain, settings) 93 | if existing_provider and existing_provider != settings.get('dns_provider', 'cloudflare'): 94 | dns_provider = existing_provider 95 | print(f" Using existing domain DNS provider: {dns_provider}") 96 | else: 97 | # Use smart suggestion 98 | suggested_provider, confidence = suggest_dns_provider_for_domain(domain, settings) 99 | if confidence >= 70: 100 | dns_provider = suggested_provider 101 | print(f" Using smart suggestion: {dns_provider} (confidence: {confidence}%)") 102 | else: 103 | dns_provider = settings.get('dns_provider', 'cloudflare') 104 | print(f" Using global default: {dns_provider}") 105 | 106 | # Determine account_id 107 | if provided_account_id: 108 | account_id = provided_account_id 109 | else: 110 | default_accounts = settings.get('default_accounts', {}) 111 | account_id = default_accounts.get(dns_provider, 'default') 112 | print(f" Using default account for {dns_provider}: {account_id}") 113 | 114 | # Validate account configuration 115 | account_config, used_account_id = get_dns_provider_account_config(dns_provider, account_id, settings) 116 | 117 | if account_config: 118 | print(f" ✓ Account configuration found: {used_account_id}") 119 | 120 | # Check if results match expectations 121 | provider_match = dns_provider == expected_provider 122 | account_match = used_account_id == expected_account 123 | 124 | if provider_match and account_match: 125 | print(f" ✓ Result matches expectations: {dns_provider}/{used_account_id}") 126 | else: 127 | print(f" ✗ Result mismatch:") 128 | print(f" Expected: {expected_provider}/{expected_account}") 129 | print(f" Got: {dns_provider}/{used_account_id}") 130 | else: 131 | print(f" ✗ No account configuration found for {dns_provider}/{account_id}") 132 | 133 | print("\nSimulation completed!") 134 | 135 | if __name__ == '__main__': 136 | simulate_certificate_creation() 137 | -------------------------------------------------------------------------------- /test_shell_executor.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test ShellExecutor and MockShellExecutor integration 3 | """ 4 | import pytest 5 | from modules.core.shell import ShellExecutor, MockShellExecutor 6 | 7 | 8 | def test_shell_executor_real(): 9 | """Test real ShellExecutor""" 10 | executor = ShellExecutor() 11 | result = executor.run(['echo', 'hello'], capture_output=True, text=True) 12 | assert result.returncode == 0 13 | assert 'hello' in result.stdout 14 | 15 | 16 | def test_mock_shell_executor_success(): 17 | """Test MockShellExecutor with success""" 18 | mock = MockShellExecutor() 19 | mock.set_next_result(returncode=0, stdout="success", stderr="") 20 | 21 | result = mock.run(['certbot', 'certonly'], capture_output=True, text=True) 22 | assert result.returncode == 0 23 | assert result.stdout == "success" 24 | assert result.stderr == "" 25 | 26 | 27 | def test_mock_shell_executor_failure(): 28 | """Test MockShellExecutor with failure""" 29 | mock = MockShellExecutor() 30 | mock.set_next_result(returncode=1, stdout="", stderr="error occurred") 31 | 32 | result = mock.run(['certbot', 'certonly'], capture_output=True, text=True) 33 | assert result.returncode == 1 34 | assert result.stderr == "error occurred" 35 | 36 | 37 | def test_mock_shell_executor_timeout(): 38 | """Test MockShellExecutor with timeout""" 39 | import subprocess 40 | 41 | mock = MockShellExecutor() 42 | mock.set_next_result(should_timeout=True) 43 | 44 | with pytest.raises(subprocess.TimeoutExpired): 45 | mock.run(['certbot', 'certonly'], timeout=1) 46 | 47 | 48 | def test_mock_shell_executor_queue(): 49 | """Test MockShellExecutor with multiple queued results""" 50 | mock = MockShellExecutor() 51 | mock.set_next_result(returncode=0, stdout="first") 52 | mock.set_next_result(returncode=0, stdout="second") 53 | mock.set_next_result(returncode=1, stderr="third") 54 | 55 | result1 = mock.run(['cmd1'], capture_output=True, text=True) 56 | assert result1.stdout == "first" 57 | 58 | result2 = mock.run(['cmd2'], capture_output=True, text=True) 59 | assert result2.stdout == "second" 60 | 61 | result3 = mock.run(['cmd3'], capture_output=True, text=True) 62 | assert result3.returncode == 1 63 | assert result3.stderr == "third" 64 | 65 | 66 | def test_certificate_manager_with_mock_executor(): 67 | """Test CertificateManager with MockShellExecutor""" 68 | from pathlib import Path 69 | import tempfile 70 | from modules.core.certificates import CertificateManager 71 | from modules.core.settings import SettingsManager 72 | from modules.core.dns_providers import DNSManager 73 | from modules.core.file_operations import FileOperations 74 | 75 | # Create temp directory 76 | with tempfile.TemporaryDirectory() as tmpdir: 77 | tmppath = Path(tmpdir) 78 | cert_dir = tmppath / "certs" 79 | data_dir = tmppath / "data" 80 | backup_dir = tmppath / "backups" 81 | logs_dir = tmppath / "logs" 82 | 83 | for d in [cert_dir, data_dir, backup_dir, logs_dir]: 84 | d.mkdir(parents=True, exist_ok=True) 85 | 86 | # Initialize managers 87 | file_ops = FileOperations(cert_dir, data_dir, backup_dir, logs_dir) 88 | settings_file = data_dir / "settings.json" 89 | settings_manager = SettingsManager(file_ops, settings_file) 90 | dns_manager = DNSManager(settings_manager) 91 | 92 | # Create mock executor 93 | mock_executor = MockShellExecutor() 94 | 95 | # Initialize CertificateManager with mock 96 | cert_manager = CertificateManager( 97 | cert_dir=cert_dir, 98 | settings_manager=settings_manager, 99 | dns_manager=dns_manager, 100 | shell_executor=mock_executor 101 | ) 102 | 103 | # Verify the executor is set 104 | assert cert_manager.shell_executor is mock_executor 105 | assert isinstance(cert_manager.shell_executor, MockShellExecutor) 106 | 107 | # Verify it's actually used when we call methods 108 | mock_executor.add_response("certbot", returncode=0, stdout="success") 109 | result = mock_executor.run(["certbot", "certonly"], capture_output=True, text=True) 110 | assert result.returncode == 0 111 | assert mock_executor.call_count == 1 112 | 113 | 114 | def test_shell_executor_dependency_injection(): 115 | """Verify ShellExecutor is properly injected and used""" 116 | from pathlib import Path 117 | import tempfile 118 | from modules.core.certificates import CertificateManager 119 | from modules.core.settings import SettingsManager 120 | from modules.core.dns_providers import DNSManager 121 | from modules.core.file_operations import FileOperations 122 | 123 | with tempfile.TemporaryDirectory() as tmpdir: 124 | tmppath = Path(tmpdir) 125 | cert_dir = tmppath / "certs" 126 | data_dir = tmppath / "data" 127 | backup_dir = tmppath / "backups" 128 | logs_dir = tmppath / "logs" 129 | 130 | for d in [cert_dir, data_dir, backup_dir, logs_dir]: 131 | d.mkdir(parents=True, exist_ok=True) 132 | 133 | file_ops = FileOperations(cert_dir, data_dir, backup_dir, logs_dir) 134 | settings_file = data_dir / "settings.json" 135 | settings_manager = SettingsManager(file_ops, settings_file) 136 | dns_manager = DNSManager(settings_manager) 137 | 138 | # Test 1: Default executor 139 | cert_manager1 = CertificateManager( 140 | cert_dir=cert_dir, 141 | settings_manager=settings_manager, 142 | dns_manager=dns_manager 143 | ) 144 | assert isinstance(cert_manager1.shell_executor, ShellExecutor) 145 | assert not isinstance(cert_manager1.shell_executor, MockShellExecutor) 146 | 147 | # Test 2: Injected mock executor 148 | mock = MockShellExecutor() 149 | cert_manager2 = CertificateManager( 150 | cert_dir=cert_dir, 151 | settings_manager=settings_manager, 152 | dns_manager=dns_manager, 153 | shell_executor=mock 154 | ) 155 | assert cert_manager2.shell_executor is mock 156 | assert isinstance(cert_manager2.shell_executor, MockShellExecutor) 157 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 📝 CertMate Client Certificates - Changelog 2 | 3 | All notable changes to the Client Certificates feature will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | --- 9 | 10 | ## [1.0.0] - 2024-10-30 11 | 12 | ### ✨ Added 13 | 14 | #### Phase 1: CA Foundation 15 | - **PrivateCAGenerator** - Self-signed Certificate Authority with 4096-bit RSA keys 16 | - **CSRHandler** - Certificate Signing Request validation and creation 17 | - Support for 2048 and 4096-bit RSA key sizes 18 | - Secure key storage with proper file permissions (0600) 19 | - CA backup and metadata management 20 | 21 | #### Phase 2: Client Certificate Engine 22 | - **ClientCertificateManager** - Complete certificate lifecycle management 23 | - Create client certificates with automatic signing 24 | - List certificates with optional filtering 25 | - Filter by usage type (api-mtls, vpn, custom) 26 | - Filter by revocation status 27 | - Search by common name 28 | - Revoke certificates with audit trail 29 | - Renew certificates (same CN, new serial) 30 | - Auto-renewal scheduling (daily at 3 AM) 31 | - Support for 30k+ concurrent certificates 32 | - Metadata storage (JSON per certificate) 33 | - Certificate statistics and usage breakdown 34 | 35 | #### Phase 3: UI & Advanced Features 36 | - **Web Dashboard** at `/client-certificates` 37 | - Statistics panel (total, active, revoked, by-usage) 38 | - Single certificate creation form 39 | - Bulk CSV import with drag-drop 40 | - Certificate table with search and filters 41 | - Download modal for cert/key/csr files 42 | - Revoke and renew action buttons 43 | - Dark mode support 44 | - Fully responsive design 45 | 46 | - **OCSP Responder** - Online Certificate Status Protocol 47 | - Query certificate status (good/revoked/unknown) 48 | - Generate OCSP responses 49 | - Real-time status lookups 50 | 51 | - **CRL Manager** - Certificate Revocation List 52 | - Generate CRL with revoked serials 53 | - PEM format distribution 54 | - DER format conversion 55 | - CRL metadata and info retrieval 56 | 57 | - **REST API** - 10 endpoints across 3 namespaces 58 | - POST /api/client-certs/create - Create certificate 59 | - GET /api/client-certs - List certificates 60 | - GET /api/client-certs/ - Get metadata 61 | - GET /api/client-certs//download/ - Download files 62 | - POST /api/client-certs//revoke - Revoke 63 | - POST /api/client-certs//renew - Renew 64 | - GET /api/client-certs/stats - Statistics 65 | - POST /api/client-certs/batch - Batch import 66 | - GET /api/ocsp/status/ - OCSP query 67 | - GET /api/crl/download/ - CRL download 68 | 69 | #### Phase 4: Easy Wins 70 | - **Audit Logging** - Comprehensive operation tracking 71 | - JSON format logging 72 | - Persistent audit file 73 | - User and IP address tracking 74 | - Query by resource or time window 75 | - Covers: create, revoke, renew, download, batch ops 76 | 77 | - **Rate Limiting** - API protection 78 | - Configurable per-endpoint limits 79 | - Default: 100 req/min (global) 80 | - Certificate creation: 30 req/min 81 | - Batch operations: 10 req/min 82 | - OCSP: 200 req/min 83 | - HTTP 429 responses with Retry-After 84 | 85 | #### Testing & Documentation 86 | - Comprehensive E2E test suite (27 tests) 87 | - Full API documentation 88 | - Architecture guide 89 | - User guide 90 | - Quick start examples 91 | - Rate limiting information 92 | - Audit logging details 93 | 94 | ### 🔐 Security Features 95 | - 4096-bit RSA for CA 96 | - SHA256 signature algorithm 97 | - Bearer token authentication 98 | - Rate limiting 99 | - Audit logging 100 | - Proper file permissions 101 | - Secure key storage 102 | 103 | ### 📊 Performance 104 | - Support for 30k+ certificates 105 | - Efficient multi-filter queries 106 | - Batch operations (100-30k certs) 107 | - Auto-renewal scheduling 108 | - Low memory footprint 109 | 110 | ### 🧪 Quality Assurance 111 | - 27/27 tests passing 112 | - 100% test coverage for core features 113 | - Comprehensive error handling 114 | - All deprecation warnings fixed 115 | - Production-ready code 116 | 117 | ### 📖 Documentation 118 | - Complete API reference 119 | - System architecture guide 120 | - User guide with examples 121 | - Inline code documentation 122 | - README with quick links 123 | 124 | --- 125 | 126 | ## [Unreleased] 127 | 128 | ### Planned for Future Releases 129 | 130 | #### Phase 4.1: Advanced Security 131 | - [ ] CA password protection 132 | - [ ] Hardware token support (PKCS#11) 133 | - [ ] Key rotation policies 134 | - [ ] Advanced audit filtering 135 | 136 | #### Phase 4.2: Enterprise Features 137 | - [ ] Role-based access control 138 | - [ ] LDAP/AD integration 139 | - [ ] Multi-tenancy support 140 | - [ ] Webhook notifications 141 | 142 | #### Phase 4.3: Integration & Automation 143 | - [ ] Certificate validation webhooks 144 | - [ ] Expiration notifications 145 | - [ ] Auto-renewal alerts 146 | - [ ] Prometheus metrics 147 | 148 | --- 149 | 150 | ## Upgrade Guide 151 | 152 | ### From Unsupported Version → 1.0.0 153 | 154 | No previous versions exist. This is the initial release. 155 | 156 | ### Future Upgrades 157 | 158 | ```bash 159 | # Backup existing certificates 160 | tar -czf backup-client-certs.tar.gz data/certs/ 161 | 162 | # Update code 163 | git pull origin main 164 | 165 | # Restart service 166 | python app.py 167 | ``` 168 | 169 | --- 170 | 171 | ## Known Limitations 172 | 173 | ### Current Version (1.0.0) 174 | 175 | 1. **No Standalone CSR Signing** 176 | - Cannot sign external CSRs yet 177 | - Plan: Support CSR submission in v1.1 178 | 179 | 2. **No Certificate Templates** 180 | - Each cert customized individually 181 | - Plan: Add templates in v1.1 182 | 183 | 3. **In-Memory Rate Limiting** 184 | - Single-instance only 185 | - Plan: Redis support in v1.2 186 | 187 | 4. **No Webhook Notifications** 188 | - No event callbacks yet 189 | - Plan: Add in v1.1 190 | 191 | --- 192 | 193 | ## Breaking Changes 194 | 195 | No breaking changes in 1.0.0 (initial release). 196 | 197 | --- 198 | 199 | ## Contributors 200 | 201 | - @fabriziosalmi - Lead Developer 202 | - Claude Code - Implementation & Documentation 203 | 204 | --- 205 | 206 | ## Support 207 | 208 | - 📖 [Documentation](./README.md) 209 | - 🐛 [Issue Tracker](https://github.com/fabriziosalmi/certmate/issues) 210 | - 💬 [Discussions](https://github.com/fabriziosalmi/certmate/discussions) 211 | 212 | --- 213 | 214 | ## License 215 | 216 | MIT License - See LICENSE file in repository 217 | 218 | --- 219 | 220 |
221 | 222 | [Home](../README.md) • [Docs](./README.md) • [API](./api.md) • [GitHub](https://github.com/fabriziosalmi/certmate) 223 | 224 |
225 | -------------------------------------------------------------------------------- /test_dns_provider_detection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test DNS provider detection with actual settings 4 | """ 5 | 6 | import os 7 | import sys 8 | import json 9 | import requests 10 | from pathlib import Path 11 | 12 | # Add the current directory to Python path 13 | sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) 14 | 15 | # Configuration 16 | BASE_URL = "http://localhost:8000" 17 | API_TOKEN = "3kQlbC4OQIcKriSVJ7zYlX6vJy8w0HIOD-YyNSSXuC4" 18 | HEADERS = { 19 | "Authorization": f"Bearer {API_TOKEN}", 20 | "Content-Type": "application/json" 21 | } 22 | 23 | def test_current_certificate_dns_providers(): 24 | """Test DNS provider detection for current certificates""" 25 | try: 26 | # Get current certificates 27 | response = requests.get(f"{BASE_URL}/api/certificates", headers=HEADERS, timeout=10) 28 | if response.status_code == 200: 29 | certificates = response.json() 30 | 31 | print("=== Current Certificate DNS Providers ===") 32 | print(f"Found {len(certificates)} certificates:") 33 | 34 | for cert in certificates: 35 | domain = cert.get('domain', 'Unknown') 36 | dns_provider = cert.get('dns_provider', 'Unknown') 37 | exists = cert.get('exists', False) 38 | 39 | print(f" {domain}") 40 | print(f" DNS Provider: {dns_provider}") 41 | print(f" Certificate Exists: {exists}") 42 | print() 43 | 44 | # Check specific domains 45 | expected_providers = { 46 | "test2.audiolibri.org": "cloudflare", 47 | "cf-test1.audiolibri.org": "cloudflare", 48 | "cf-test2.audiolibri.org": "cloudflare", 49 | "aws-test3.test.certmate.org": "route53" 50 | } 51 | 52 | print("=== DNS Provider Validation ===") 53 | for domain, expected_provider in expected_providers.items(): 54 | found = False 55 | for cert in certificates: 56 | if cert.get('domain') == domain: 57 | actual_provider = cert.get('dns_provider') 58 | status = "✓" if actual_provider == expected_provider else "✗" 59 | print(f"{status} {domain}: {actual_provider} (expected: {expected_provider})") 60 | found = True 61 | break 62 | 63 | if not found: 64 | print(f"✗ {domain}: NOT FOUND (expected: {expected_provider})") 65 | 66 | return True 67 | else: 68 | print(f"Failed to get certificates: {response.status_code}") 69 | return False 70 | 71 | except Exception as e: 72 | print(f"Error testing DNS providers: {e}") 73 | return False 74 | 75 | def test_settings_structure(): 76 | """Test the settings structure""" 77 | try: 78 | response = requests.get(f"{BASE_URL}/api/settings", headers=HEADERS, timeout=10) 79 | if response.status_code == 200: 80 | settings = response.json() 81 | 82 | print("=== Settings Structure Analysis ===") 83 | print(f"Global DNS Provider: {settings.get('dns_provider', 'Not set')}") 84 | print(f"Number of domains: {len(settings.get('domains', []))}") 85 | 86 | domains = settings.get('domains', []) 87 | print("\nDomains configuration:") 88 | for i, domain in enumerate(domains): 89 | if isinstance(domain, dict): 90 | print(f" {i+1}. {domain.get('domain', 'Unknown')}") 91 | print(f" DNS Provider: {domain.get('dns_provider', 'Not set')}") 92 | print(f" Account ID: {domain.get('account_id', 'Not set')}") 93 | elif isinstance(domain, str): 94 | print(f" {i+1}. {domain} (old format - will inherit global DNS provider)") 95 | else: 96 | print(f" {i+1}. Invalid domain format: {domain}") 97 | 98 | # Check DNS providers structure 99 | dns_providers = settings.get('dns_providers', {}) 100 | print(f"\nConfigured DNS Providers: {list(dns_providers.keys())}") 101 | 102 | for provider, accounts in dns_providers.items(): 103 | if isinstance(accounts, dict): 104 | print(f" {provider}: {list(accounts.keys())}") 105 | else: 106 | print(f" {provider}: {accounts} (old format)") 107 | 108 | # Check default accounts 109 | default_accounts = settings.get('default_accounts', {}) 110 | print(f"\nDefault Accounts: {default_accounts}") 111 | 112 | return True 113 | else: 114 | print(f"Failed to get settings: {response.status_code}") 115 | return False 116 | 117 | except Exception as e: 118 | print(f"Error testing settings: {e}") 119 | return False 120 | 121 | def main(): 122 | """Main test function""" 123 | print("=== DNS Provider Detection Test ===") 124 | 125 | # Test API connectivity 126 | try: 127 | response = requests.get(f"{BASE_URL}/health", timeout=10) 128 | if response.status_code == 200: 129 | print("✓ API is accessible") 130 | else: 131 | print("✗ API is not accessible") 132 | return False 133 | except Exception as e: 134 | print(f"✗ API connection failed: {e}") 135 | return False 136 | 137 | # Test authentication 138 | try: 139 | response = requests.get(f"{BASE_URL}/api/settings", headers=HEADERS, timeout=10) 140 | if response.status_code == 200: 141 | print("✓ API authentication successful") 142 | else: 143 | print("✗ API authentication failed") 144 | return False 145 | except Exception as e: 146 | print(f"✗ API authentication failed: {e}") 147 | return False 148 | 149 | print() 150 | 151 | # Test settings structure 152 | if not test_settings_structure(): 153 | print("Settings structure test failed") 154 | return False 155 | 156 | print() 157 | 158 | # Test current certificate DNS providers 159 | if not test_current_certificate_dns_providers(): 160 | print("Certificate DNS provider test failed") 161 | return False 162 | 163 | print("\n=== Test Complete ===") 164 | return True 165 | 166 | if __name__ == '__main__': 167 | success = main() 168 | sys.exit(0 if success else 1) 169 | -------------------------------------------------------------------------------- /DOCKER_DEPLOYMENT.md: -------------------------------------------------------------------------------- 1 | # Docker Build and Deployment Guide 2 | 3 | ## Building a Secure Docker Image 4 | 5 | This guide explains how to build and deploy CertMate as a Docker container while ensuring no sensitive environment variables or secrets are included in the image. 6 | 7 | ### Prerequisites 8 | 9 | 1. Docker installed on your system 10 | 2. DockerHub account (if pushing to DockerHub) 11 | 3. No `.env` file in the build context (handled by `.dockerignore`) 12 | 13 | ### Security Features 14 | 15 | - **No secrets in image**: The `.dockerignore` file excludes all `.env` files and sensitive data 16 | - **Runtime configuration**: Environment variables are provided at container runtime, not build time 17 | - **Minimal attack surface**: Only essential application files are included in the image 18 | 19 | ### Building the Docker Image 20 | 21 | #### 1. Local Build 22 | 23 | ```bash 24 | # Build the image locally 25 | docker build -t certmate:latest . 26 | 27 | # Or with a specific tag 28 | docker build -t certmate:v1.0.0 . 29 | ``` 30 | 31 | #### 2. Build for DockerHub 32 | 33 | ```bash 34 | # Replace 'yourusername' with your DockerHub username 35 | docker build -t yourusername/certmate:latest . 36 | docker build -t yourusername/certmate:v1.0.0 . 37 | ``` 38 | 39 | ### Pushing to DockerHub 40 | 41 | #### 1. Login to DockerHub 42 | 43 | ```bash 44 | docker login 45 | ``` 46 | 47 | #### 2. Push the Images 48 | 49 | ```bash 50 | # Push latest tag 51 | docker push yourusername/certmate:latest 52 | 53 | # Push version tag 54 | docker push yourusername/certmate:v1.0.0 55 | ``` 56 | 57 | ### Running the Container 58 | 59 | #### 1. Create Environment File (Host System) 60 | 61 | Create a `.env` file on your host system (NOT in the Docker image): 62 | 63 | ```bash 64 | # Create .env file with your settings 65 | cat > .env << 'EOF' 66 | SECRET_KEY=your-super-secret-key-here 67 | ADMIN_TOKEN=your-admin-token-here 68 | CLOUDFLARE_EMAIL=your-email@example.com 69 | CLOUDFLARE_API_TOKEN=your-cloudflare-api-token 70 | LOG_LEVEL=INFO 71 | EOF 72 | ``` 73 | 74 | #### 2. Run with Environment File 75 | 76 | ```bash 77 | # Run with environment file 78 | docker run -d \ 79 | --name certmate \ 80 | --env-file .env \ 81 | -p 8000:8000 \ 82 | -v certmate_certificates:/app/certificates \ 83 | -v certmate_data:/app/data \ 84 | -v certmate_logs:/app/logs \ 85 | yourusername/certmate:latest 86 | ``` 87 | 88 | #### 3. Run with Direct Environment Variables 89 | 90 | ```bash 91 | # Run with individual environment variables 92 | docker run -d \ 93 | --name certmate \ 94 | -e SECRET_KEY="your-super-secret-key-here" \ 95 | -e ADMIN_TOKEN="your-admin-token-here" \ 96 | -e CLOUDFLARE_EMAIL="your-email@example.com" \ 97 | -e CLOUDFLARE_API_TOKEN="your-cloudflare-api-token" \ 98 | -e LOG_LEVEL="INFO" \ 99 | -p 8000:8000 \ 100 | -v certmate_certificates:/app/certificates \ 101 | -v certmate_data:/app/data \ 102 | -v certmate_logs:/app/logs \ 103 | yourusername/certmate:latest 104 | ``` 105 | 106 | ### Docker Compose Deployment 107 | 108 | #### 1. Create docker-compose.yml 109 | 110 | ```yaml 111 | version: '3.8' 112 | 113 | services: 114 | certmate: 115 | image: yourusername/certmate:latest 116 | container_name: certmate 117 | ports: 118 | - "8000:8000" 119 | environment: 120 | - SECRET_KEY=${SECRET_KEY} 121 | - ADMIN_TOKEN=${ADMIN_TOKEN} 122 | - CLOUDFLARE_EMAIL=${CLOUDFLARE_EMAIL} 123 | - CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN} 124 | - LOG_LEVEL=${LOG_LEVEL:-INFO} 125 | volumes: 126 | - certmate_certificates:/app/certificates 127 | - certmate_data:/app/data 128 | - certmate_logs:/app/logs 129 | restart: unless-stopped 130 | healthcheck: 131 | test: ["CMD", "curl", "-f", "http://localhost:8000/health"] 132 | interval: 30s 133 | timeout: 10s 134 | retries: 3 135 | start_period: 40s 136 | 137 | volumes: 138 | certmate_certificates: 139 | certmate_data: 140 | certmate_logs: 141 | ``` 142 | 143 | #### 2. Run with Docker Compose 144 | 145 | ```bash 146 | # With .env file in the same directory 147 | docker-compose up -d 148 | 149 | # Or specify environment file 150 | docker-compose --env-file /path/to/.env up -d 151 | ``` 152 | 153 | ### Security Verification 154 | 155 | #### 1. Verify No Secrets in Image 156 | 157 | ```bash 158 | # Inspect the image layers 159 | docker history yourusername/certmate:latest 160 | 161 | # Check for environment variables in image 162 | docker inspect yourusername/certmate:latest | grep -i env 163 | 164 | # Run container and check for .env files 165 | docker run --rm yourusername/certmate:latest find / -name "*.env" 2>/dev/null 166 | ``` 167 | 168 | #### 2. Verify Container Runtime 169 | 170 | ```bash 171 | # Check running container environment 172 | docker exec certmate env | grep -E "(SECRET_KEY|ADMIN_TOKEN|CLOUDFLARE)" 173 | 174 | # Check health status 175 | docker exec certmate curl -f http://localhost:8000/health 176 | ``` 177 | 178 | ### Environment Variables Reference 179 | 180 | | Variable | Required | Description | Example | 181 | |----------|----------|-------------|---------| 182 | | `SECRET_KEY` | Yes | Flask secret key for sessions | `your-super-secret-key-here` | 183 | | `ADMIN_TOKEN` | Yes | Authentication token for admin access | `your-admin-token-here` | 184 | | `CLOUDFLARE_EMAIL` | Yes | Cloudflare account email | `user@example.com` | 185 | | `CLOUDFLARE_API_TOKEN` | Yes | Cloudflare API token with DNS permissions | `your-api-token` | 186 | | `LOG_LEVEL` | No | Logging level | `INFO` (default), `DEBUG`, `WARNING`, `ERROR` | 187 | 188 | ### Production Deployment Tips 189 | 190 | 1. **Use secrets management**: In production, use Docker secrets, Kubernetes secrets, or a secrets manager 191 | 2. **Enable TLS**: Run behind a reverse proxy with TLS termination 192 | 3. **Monitor resources**: Set appropriate CPU and memory limits 193 | 4. **Backup volumes**: Regularly backup the certificate and data volumes 194 | 5. **Update regularly**: Keep the image updated with latest security patches 195 | 196 | ### Troubleshooting 197 | 198 | #### Container Won't Start 199 | ```bash 200 | # Check logs 201 | docker logs certmate 202 | 203 | # Check if environment variables are set 204 | docker exec certmate env 205 | ``` 206 | 207 | #### Health Check Fails 208 | ```bash 209 | # Check application logs 210 | docker logs certmate 211 | 212 | # Test health endpoint manually 213 | docker exec certmate curl -v http://localhost:8000/health 214 | ``` 215 | 216 | #### Permission Issues 217 | ```bash 218 | # Check file permissions in container 219 | docker exec certmate ls -la /app/certificates 220 | docker exec certmate ls -la /app/data 221 | ``` 222 | 223 | ## Summary 224 | 225 | This setup ensures: 226 | - ✅ No secrets are baked into the Docker image 227 | - ✅ Environment variables are provided at runtime 228 | - ✅ Sensitive files are excluded via `.dockerignore` 229 | - ✅ Image can be safely pushed to public registries 230 | - ✅ Production-ready deployment configuration 231 | -------------------------------------------------------------------------------- /INSTALLATION.md: -------------------------------------------------------------------------------- 1 | # Installation Guide 2 | 3 | This guide will help you install CertMate with multi-DNS provider support. 4 | 5 | ## Prerequisites 6 | 7 | - Python 3.9 or higher 8 | - pip (Python package manager) 9 | - Docker (optional, for containerized deployment) 10 | 11 | ## Method 1: Direct Installation 12 | 13 | ### 1. Clone the Repository 14 | 15 | ```bash 16 | git clone https://github.com/fabriziosalmi/certmate.git 17 | cd certmate 18 | ``` 19 | 20 | ### 2. Create Virtual Environment (Recommended) 21 | 22 | ```bash 23 | python3 -m venv venv 24 | source venv/bin/activate # On Windows: venv\Scripts\activate 25 | ``` 26 | 27 | ### 3. Install Dependencies 28 | 29 | ```bash 30 | pip install -r requirements.txt 31 | ``` 32 | 33 | ### 4. Validate Installation 34 | 35 | ```bash 36 | python validate_dependencies.py 37 | ``` 38 | 39 | ### 5. Configure Environment 40 | 41 | Create a `.env` file: 42 | 43 | ```bash 44 | cp .env.example .env 45 | # Edit .env with your settings 46 | ``` 47 | 48 | ### 6. Start the Application 49 | 50 | ```bash 51 | python app.py 52 | ``` 53 | 54 | ## Method 2: Docker Installation 55 | 56 | ### 1. Using Docker Compose (Recommended) 57 | 58 | ```bash 59 | git clone https://github.com/fabriziosalmi/certmate.git 60 | cd certmate 61 | docker-compose up -d 62 | ``` 63 | 64 | ### 2. Using Docker Build 65 | 66 | ```bash 67 | git clone https://github.com/fabriziosalmi/certmate.git 68 | cd certmate 69 | docker build -t certmate . 70 | docker run -p 8000:8000 --env-file .env -v ./certificates:/app/certificates certmate 71 | ``` 72 | 73 | ## Troubleshooting 74 | 75 | ### Common Issues 76 | 77 | #### 1. DNS Plugin Version Conflicts 78 | 79 | If you encounter version conflicts, use these specific versions: 80 | 81 | ```txt 82 | certbot==4.1.1 83 | certbot-dns-cloudflare==4.1.1 84 | certbot-dns-route53==4.1.1 85 | certbot-dns-azure==2.6.1 86 | certbot-dns-google==4.1.1 87 | certbot-dns-powerdns==0.2.1 88 | ``` 89 | 90 | **Important**: Most DNS plugins require Certbot 4.1.1. The Azure DNS plugin has independent versioning (2.6.1) and PowerDNS is a newer plugin (0.2.1). 91 | 92 | #### 2. Missing System Dependencies 93 | 94 | **Ubuntu/Debian:** 95 | ```bash 96 | sudo apt update 97 | sudo apt install python3-dev python3-venv build-essential libssl-dev libffi-dev 98 | ``` 99 | 100 | **CentOS/RHEL/Rocky:** 101 | ```bash 102 | sudo yum install python3-devel gcc openssl-devel libffi-devel 103 | ``` 104 | 105 | **macOS:** 106 | ```bash 107 | brew install python3 openssl libffi 108 | ``` 109 | 110 | #### 3. Azure DNS Plugin Issues 111 | 112 | The Azure DNS plugin has a different versioning scheme. If you encounter issues: 113 | 114 | ```bash 115 | pip install certbot-dns-azure==2.6.1 --force-reinstall 116 | ``` 117 | 118 | #### 4. PowerDNS Plugin Issues 119 | 120 | PowerDNS plugin is newer and has limited versions: 121 | 122 | ```bash 123 | pip install certbot-dns-powerdns==0.2.1 124 | ``` 125 | 126 | #### 5. Google Cloud DNS Setup 127 | 128 | Make sure you have the Google Cloud SDK dependencies: 129 | 130 | ```bash 131 | pip install google-cloud-dns==0.35.0 132 | ``` 133 | 134 | ### Manual Dependency Installation 135 | 136 | If automatic installation fails, install DNS providers individually: 137 | 138 | ```bash 139 | # Core certbot 140 | pip install certbot==4.1.1 141 | 142 | # Cloudflare 143 | pip install certbot-dns-cloudflare==4.1.1 144 | 145 | # AWS Route53 146 | pip install certbot-dns-route53==4.1.1 boto3==1.35.76 147 | 148 | # Azure DNS 149 | pip install certbot-dns-azure==2.6.1 azure-identity==1.19.0 azure-mgmt-dns==8.1.0 150 | 151 | # Google Cloud DNS 152 | pip install certbot-dns-google==4.1.1 google-cloud-dns==0.35.0 153 | 154 | # PowerDNS 155 | pip install certbot-dns-powerdns==0.2.1 156 | ``` 157 | 158 | ### Validation Commands 159 | 160 | ```bash 161 | # Check if all dependencies are installed 162 | python validate_dependencies.py 163 | 164 | # Test the API 165 | python test_dns_providers.py 166 | 167 | # Check certbot plugins 168 | certbot plugins --text 169 | 170 | # Verify service is running 171 | curl -X GET http://localhost:5000/api/health 172 | ``` 173 | 174 | ## DNS Provider Setup 175 | 176 | ### Cloudflare 177 | 178 | 1. Go to [Cloudflare Dashboard](https://dash.cloudflare.com/profile/api-tokens) 179 | 2. Create a new API token 180 | 3. Set permissions: `Zone:DNS:Edit` 181 | 4. Add the token to your settings 182 | 183 | ### AWS Route53 184 | 185 | 1. Create IAM user with Route53 permissions 186 | 2. Generate access keys 187 | 3. Add credentials to settings 188 | 189 | ### Azure DNS 190 | 191 | 1. Create a Service Principal 192 | 2. Assign DNS Zone Contributor role 193 | 3. Get subscription details and credentials 194 | 195 | ### Google Cloud DNS 196 | 197 | 1. Create a Service Account 198 | 2. Assign DNS Administrator role 199 | 3. Download JSON key file 200 | 201 | ### PowerDNS 202 | 203 | 1. Enable API in PowerDNS configuration 204 | 2. Set API key 205 | 3. Note the API URL 206 | 207 | ## Environment Variables 208 | 209 | ```bash 210 | # API Authentication 211 | API_BEARER_TOKEN=your_secure_token_here 212 | 213 | # DNS Providers (choose one or multiple) 214 | CLOUDFLARE_TOKEN=your_cloudflare_token 215 | AWS_ACCESS_KEY_ID=your_aws_access_key 216 | AWS_SECRET_ACCESS_KEY=your_aws_secret_key 217 | AZURE_SUBSCRIPTION_ID=your_azure_subscription 218 | AZURE_TENANT_ID=your_azure_tenant 219 | AZURE_CLIENT_ID=your_azure_client 220 | AZURE_CLIENT_SECRET=your_azure_secret 221 | GOOGLE_PROJECT_ID=your_gcp_project 222 | POWERDNS_API_URL=https://your-powerdns:8081 223 | POWERDNS_API_KEY=your_powerdns_key 224 | 225 | # Optional 226 | SECRET_KEY=your_flask_secret_key 227 | ``` 228 | 229 | ## Production Deployment 230 | 231 | ### Using Gunicorn 232 | 233 | ```bash 234 | gunicorn --bind 0.0.0.0:8000 --workers 4 app:app 235 | ``` 236 | 237 | ### Using systemd 238 | 239 | Create `/etc/systemd/system/certmate.service`: 240 | 241 | ```ini 242 | [Unit] 243 | Description=CertMate SSL Certificate Manager 244 | After=network.target 245 | 246 | [Service] 247 | Type=simple 248 | User=certmate 249 | WorkingDirectory=/opt/certmate 250 | Environment=PATH=/opt/certmate/venv/bin 251 | ExecStart=/opt/certmate/venv/bin/gunicorn --bind 0.0.0.0:8000 --workers 4 app:app 252 | Restart=always 253 | 254 | [Install] 255 | WantedBy=multi-user.target 256 | ``` 257 | 258 | Enable and start: 259 | 260 | ```bash 261 | sudo systemctl enable certmate 262 | sudo systemctl start certmate 263 | ``` 264 | 265 | ### Using Docker in Production 266 | 267 | ```yaml 268 | version: '3.8' 269 | services: 270 | certmate: 271 | build: . 272 | ports: 273 | - "8000:8000" 274 | environment: 275 | - API_BEARER_TOKEN=${API_BEARER_TOKEN} 276 | - CLOUDFLARE_TOKEN=${CLOUDFLARE_TOKEN} 277 | volumes: 278 | - ./certificates:/app/certificates 279 | - ./data:/app/data 280 | restart: unless-stopped 281 | ``` 282 | 283 | ## Support 284 | 285 | If you encounter issues: 286 | 287 | 1. Run the validation script: `python validate_dependencies.py` 288 | 2. Check the logs for specific errors 289 | 3. Verify your DNS provider credentials 290 | 4. Test with the test script: `python test_dns_providers.py` 291 | 5. Check our documentation: [DNS_PROVIDERS.md](DNS_PROVIDERS.md) 292 | -------------------------------------------------------------------------------- /API_TESTING.md: -------------------------------------------------------------------------------- 1 | # CertMate API Testing Suite 2 | 3 | This directory contains comprehensive testing tools for CertMate API endpoints. Use these before committing to ensure all endpoints are working correctly. 4 | 5 | ## Quick Start 6 | 7 | ### 1. Basic Quick Test (Recommended) 8 | ```bash 9 | # Run this before every commit! 10 | ./quick_test.sh 11 | ``` 12 | 13 | ### 2. Full Test with Manual Token 14 | ```bash 15 | python3 test_all_endpoints.py --token YOUR_API_TOKEN 16 | ``` 17 | 18 | ### 3. Auto-load Token from Settings 19 | ```bash 20 | python3 test_all_endpoints.py --auto-token 21 | ``` 22 | 23 | ## Test Scripts 24 | 25 | ### `quick_test.sh` 26 | **🚀 Use this for daily development!** 27 | 28 | - Checks if server is running 29 | - Automatically loads API token from `data/settings.json` 30 | - Runs all endpoint tests 31 | - Provides clear pass/fail status 32 | - Perfect for pre-commit validation 33 | 34 | ```bash 35 | ./quick_test.sh # Test all endpoints 36 | ./quick_test.sh --public-only # Test only public endpoints 37 | ``` 38 | 39 | ### `test_all_endpoints.py` 40 | **🔧 Advanced testing with options** 41 | 42 | The main testing script with comprehensive options: 43 | 44 | ```bash 45 | # Basic usage 46 | python3 test_all_endpoints.py 47 | 48 | # With custom server URL 49 | python3 test_all_endpoints.py --url http://192.168.1.100:8000 50 | 51 | # With manual API token 52 | python3 test_all_endpoints.py --token your-api-bearer-token 53 | 54 | # Auto-load token from settings.json 55 | python3 test_all_endpoints.py --auto-token 56 | 57 | # Test only public endpoints (no auth needed) 58 | python3 test_all_endpoints.py --public-only 59 | 60 | # Quick test of essential endpoints only 61 | python3 test_all_endpoints.py --quick --auto-token 62 | ``` 63 | 64 | ## What Gets Tested 65 | 66 | ### ✅ Health Endpoints (No Auth Required) 67 | - `GET /api/health` - API health check 68 | - `GET /health` - Web health check 69 | 70 | ### ✅ Settings Endpoints (Auth Required) 71 | - `GET /api/settings` - Get current settings 72 | - `GET /api/settings/dns-providers` - Get DNS providers info 73 | - `POST /api/settings` - Update settings 74 | 75 | ### ✅ Certificate Endpoints (Auth Required) 76 | - `GET /api/certificates` - List all certificates 77 | - `POST /api/certificates/create` - Create new certificate 78 | - `GET /api/certificates/{domain}/download` - Download certificate 79 | - `POST /api/certificates/{domain}/renew` - Renew certificate 80 | 81 | ### ✅ Cache Management (Auth Required) 82 | - `GET /api/cache/stats` - Get cache statistics 83 | - `POST /api/cache/clear` - Clear deployment cache 84 | 85 | ### ✅ Backup & Restore (Auth Required) 86 | - `GET /api/backups` - List all backups 87 | - `POST /api/backups/create` - Create manual backup 88 | - `POST /api/backups/cleanup` - Cleanup old backups 89 | 90 | ### ✅ Web Interface Endpoints 91 | - `GET /` - Main dashboard 92 | - `GET /settings` - Settings page 93 | - `GET /help` - Help page 94 | - `GET /docs/` - API documentation 95 | - `GET /api/swagger.json` - Swagger specification 96 | - Various `/api/web/*` endpoints 97 | 98 | ## Expected Results 99 | 100 | ### 🟢 Success Indicators 101 | - **✅ Green checkmarks** - Endpoint working correctly 102 | - **Status codes 200/201** - Normal success responses 103 | - **Status codes 400/422** - Expected validation errors (normal) 104 | - **Status code 404** - Expected for non-existent resources 105 | 106 | ### 🔴 Failure Indicators 107 | - **❌ Red X marks** - Endpoint not working 108 | - **Connection refused** - Server not running 109 | - **401 Unauthorized** - Invalid/missing API token 110 | - **500 Internal Server Error** - Application bug 111 | 112 | ## Integration with Development Workflow 113 | 114 | ### Pre-Commit Hook (Recommended) 115 | Add this to your Git pre-commit hook: 116 | 117 | ```bash 118 | #!/bin/sh 119 | echo "Running API endpoint tests..." 120 | ./quick_test.sh 121 | if [ $? -ne 0 ]; then 122 | echo "❌ API tests failed! Commit aborted." 123 | exit 1 124 | fi 125 | echo "✅ All API tests passed!" 126 | ``` 127 | 128 | ### CI/CD Integration 129 | ```yaml 130 | # GitHub Actions example 131 | - name: Test API Endpoints 132 | run: | 133 | python app.py & 134 | sleep 5 135 | python3 test_all_endpoints.py --auto-token 136 | ``` 137 | 138 | ### Development Workflow 139 | 1. **Start development server**: `python app.py` 140 | 2. **Make your changes** 141 | 3. **Run quick test**: `./quick_test.sh` 142 | 4. **Fix any failing tests** 143 | 5. **Commit when all tests pass**: `git commit -m "..."` 144 | 145 | ## Troubleshooting 146 | 147 | ### "Server not running" Error 148 | ```bash 149 | # Make sure server is started 150 | python app.py 151 | # Or in background: python app.py & 152 | ``` 153 | 154 | ### "Could not load API token" Warning 155 | ```bash 156 | # Check if settings.json exists 157 | ls data/settings.json 158 | 159 | # Or provide token manually 160 | python3 test_all_endpoints.py --token YOUR_TOKEN 161 | ``` 162 | 163 | ### Authentication Errors (401) 164 | - Check your API token in `data/settings.json` 165 | - Verify token is correctly formatted 166 | - Ensure token has proper permissions 167 | 168 | ### Individual Endpoint Failures 169 | - Check server logs for detailed error messages 170 | - Verify required dependencies are installed 171 | - Ensure database/file permissions are correct 172 | 173 | ## Advanced Usage 174 | 175 | ### Testing Against Remote Server 176 | ```bash 177 | python3 test_all_endpoints.py \ 178 | --url https://your-production-server.com \ 179 | --token your-production-token 180 | ``` 181 | 182 | ### Custom Test Scenarios 183 | ```bash 184 | # Test only essential endpoints quickly 185 | python3 test_all_endpoints.py --quick --auto-token 186 | 187 | # Test without authentication (public endpoints only) 188 | python3 test_all_endpoints.py --public-only 189 | 190 | # Test with verbose output (modify script to add --verbose flag) 191 | ``` 192 | 193 | ## Exit Codes 194 | 195 | - **0** - All tests passed ✅ 196 | - **1** - Some tests failed ❌ 197 | 198 | Perfect for scripting and CI/CD integration! 199 | 200 | --- 201 | 202 | ## Example Output 203 | 204 | ``` 205 | 🚀 CertMate API Endpoint Test Suite 206 | Testing API at: http://127.0.0.1:8000 207 | Timestamp: 2025-01-07 10:30:15 208 | ================================================================================ 209 | 210 | 🏥 Health Check Endpoints 211 | ✅ GET /api/health API Health Check ✓ 200 212 | ✅ GET /health Web Health Check ✓ 200 213 | 214 | ⚙️ Settings Endpoints 215 | ✅ GET /api/settings Get Current Settings ✓ 200 216 | ✅ GET /api/settings/dns-providers Get DNS Providers ✓ 200 217 | ✅ POST /api/settings Update Settings ✓ 200 218 | 219 | 🔒 Certificate Endpoints 220 | ✅ GET /api/certificates List All Certificates ✓ 200 221 | ✅ POST /api/certificates/create Create Certificate ✓ 400 222 | ✅ GET /api/certificates/test.../download Download Certificate ✓ 404 223 | ✅ POST /api/certificates/test.../renew Renew Certificate ✓ 404 224 | 225 | ================================================================================ 226 | 📊 Test Summary 227 | Total Tests: 24 228 | ✅ Passed: 24 229 | ❌ Failed: 0 230 | 231 | 🎉 All tests passed! Ready to commit. 232 | ``` 233 | 234 | This testing suite will give you confidence that your API is working correctly before every commit! 🚀 235 | -------------------------------------------------------------------------------- /TESTING.md: -------------------------------------------------------------------------------- 1 | # Testing Guide for CertMate 2 | 3 | This document describes the testing framework and best practices for CertMate. 4 | 5 | ## Overview 6 | 7 | CertMate uses a comprehensive testing framework to ensure code quality and prevent regressions. Every commit should pass all tests before being merged. 8 | 9 | ## Test Structure 10 | 11 | ``` 12 | tests/ 13 | ├── __init__.py 14 | ├── conftest.py # Test configuration and fixtures 15 | ├── test_app.py # Flask application tests 16 | ├── test_api.py # API endpoint tests 17 | ├── test_certificate_management.py # Certificate handling tests 18 | ├── test_dns_providers_integration.py # DNS provider tests 19 | └── fixtures/ 20 | ├── __init__.py 21 | └── sample_settings.json 22 | ``` 23 | 24 | ## Running Tests 25 | 26 | ### Quick Start 27 | 28 | ```bash 29 | # Activate virtual environment 30 | source .venv/bin/activate 31 | 32 | # Install test dependencies 33 | pip install -r requirements-test.txt 34 | 35 | # Run all tests 36 | pytest 37 | 38 | # Run tests with coverage 39 | pytest --cov=. --cov-report=html 40 | ``` 41 | 42 | ### Using Make Commands 43 | 44 | ```bash 45 | # Run all tests 46 | make test 47 | 48 | # Run only unit tests 49 | make test-unit 50 | 51 | # Run only integration tests 52 | make test-integration 53 | 54 | # Run tests with coverage 55 | make test-coverage 56 | 57 | # Run tests in watch mode 58 | make test-watch 59 | 60 | # Run all quality checks 61 | make check 62 | ``` 63 | 64 | ## Test Categories 65 | 66 | Tests are organized with pytest markers: 67 | 68 | ### Unit Tests 69 | ```bash 70 | pytest -m "not integration and not slow" 71 | ``` 72 | Fast tests that don't require external services. 73 | 74 | ### Integration Tests 75 | ```bash 76 | pytest -m integration 77 | ``` 78 | Tests that interact with external services or complex components. 79 | 80 | ### API Tests 81 | ```bash 82 | pytest -m api 83 | ``` 84 | Tests for REST API endpoints. 85 | 86 | ### DNS Provider Tests 87 | ```bash 88 | pytest -m dns 89 | ``` 90 | Tests for DNS provider integrations. 91 | 92 | ## Test Configuration 93 | 94 | ### pytest.ini 95 | Configuration file with test settings, coverage options, and markers. 96 | 97 | ### conftest.py 98 | Contains shared fixtures: 99 | - `app`: Flask application instance for testing 100 | - `client`: Test client for API requests 101 | - `runner`: CLI test runner 102 | - `sample_settings`: Mock configuration data 103 | - `mock_certificate_data`: Sample certificate data 104 | 105 | ## Writing Tests 106 | 107 | ### Test Structure 108 | ```python 109 | import pytest 110 | from unittest.mock import patch, MagicMock 111 | 112 | def test_function_name(client, sample_settings): 113 | """Test description.""" 114 | # Arrange 115 | setup_data = {...} 116 | 117 | # Act 118 | response = client.get('/api/endpoint') 119 | 120 | # Assert 121 | assert response.status_code == 200 122 | assert 'expected_key' in response.json() 123 | ``` 124 | 125 | ### Fixtures Usage 126 | ```python 127 | def test_with_app_context(app): 128 | """Test that requires app context.""" 129 | with app.app_context(): 130 | # Test code here 131 | pass 132 | 133 | def test_api_endpoint(client): 134 | """Test API endpoint.""" 135 | response = client.get('/api/test') 136 | assert response.status_code == 200 137 | 138 | def test_with_mock_data(mock_certificate_data): 139 | """Test with mock data.""" 140 | assert mock_certificate_data['domain'] == 'test.example.com' 141 | ``` 142 | 143 | ### Mocking External Services 144 | ```python 145 | @patch('app.requests.get') 146 | def test_external_api(mock_get, client): 147 | """Test external API call.""" 148 | mock_get.return_value.json.return_value = {'status': 'success'} 149 | 150 | response = client.post('/api/certificate/request') 151 | assert response.status_code == 200 152 | ``` 153 | 154 | ## Continuous Integration 155 | 156 | ### GitHub Actions 157 | The CI pipeline runs on every push and pull request: 158 | 159 | 1. **Multiple Python Versions**: Tests on Python 3.9, 3.11, 3.12 160 | 2. **Code Quality**: Linting with flake8 161 | 3. **Security**: Security scanning with bandit 162 | 4. **Tests**: Full test suite with coverage 163 | 5. **Docker**: Test Docker build 164 | 6. **Coverage**: Upload to Codecov 165 | 166 | ### Pre-commit Hooks 167 | Install pre-commit hooks to run checks automatically: 168 | 169 | ```bash 170 | pip install pre-commit 171 | pre-commit install 172 | ``` 173 | 174 | Hooks include: 175 | - Code formatting (black, isort) 176 | - Linting (flake8) 177 | - Security checks (bandit) 178 | - Test execution 179 | 180 | ## Coverage Requirements 181 | 182 | - Maintain minimum 80% code coverage 183 | - All new features must include tests 184 | - Critical paths must have 95%+ coverage 185 | 186 | ### Viewing Coverage 187 | ```bash 188 | # Generate HTML coverage report 189 | pytest --cov=. --cov-report=html 190 | 191 | # Open coverage report 192 | open htmlcov/index.html 193 | ``` 194 | 195 | ## Test Data Management 196 | 197 | ### Fixtures 198 | - Use fixtures for reusable test data 199 | - Mock external dependencies 200 | - Create isolated test environments 201 | 202 | ### Test Database 203 | Tests use temporary directories and mock data to avoid affecting production data. 204 | 205 | ## Best Practices 206 | 207 | ### DO: 208 | - Write tests for all new features 209 | - Use descriptive test names 210 | - Test both success and failure cases 211 | - Mock external dependencies 212 | - Use appropriate test markers 213 | - Keep tests isolated and independent 214 | 215 | ### DON'T: 216 | - Test implementation details 217 | - Use real API keys in tests 218 | - Make tests dependent on each other 219 | - Ignore test failures 220 | - Skip writing tests for "simple" code 221 | 222 | ## Debugging Tests 223 | 224 | ### Verbose Output 225 | ```bash 226 | pytest -v -s # Show print statements 227 | ``` 228 | 229 | ### Debug Specific Test 230 | ```bash 231 | pytest tests/test_api.py::test_specific_function -v -s 232 | ``` 233 | 234 | ### Using pdb 235 | ```python 236 | def test_debug_example(): 237 | import pdb; pdb.set_trace() 238 | # Test code here 239 | ``` 240 | 241 | ## Performance Testing 242 | 243 | ### Load Testing 244 | For API endpoints that need performance testing: 245 | 246 | ```python 247 | import pytest 248 | import concurrent.futures 249 | 250 | @pytest.mark.slow 251 | def test_api_load(client): 252 | """Test API under load.""" 253 | def make_request(): 254 | return client.get('/api/certificates') 255 | 256 | with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: 257 | futures = [executor.submit(make_request) for _ in range(100)] 258 | responses = [f.result() for f in futures] 259 | 260 | assert all(r.status_code == 200 for r in responses) 261 | ``` 262 | 263 | ## Maintenance 264 | 265 | ### Regular Tasks 266 | - Update test dependencies monthly 267 | - Review and update test coverage goals 268 | - Clean up obsolete tests 269 | - Update CI configuration as needed 270 | 271 | ### Adding New Test Types 272 | When adding new features: 273 | 1. Create corresponding test files 274 | 2. Add appropriate markers 275 | 3. Update this documentation 276 | 4. Add to CI pipeline if needed 277 | 278 | ## Resources 279 | 280 | - [pytest documentation](https://docs.pytest.org/) 281 | - [Flask testing](https://flask.palletsprojects.com/en/2.3.x/testing/) 282 | - [Python testing best practices](https://docs.python-guide.org/writing/tests/) 283 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # 📖 CertMate Documentation 2 | 3 | Welcome to the CertMate documentation! This folder contains comprehensive guides for all features. 4 | 5 | ## 🎫 Client Certificates Documentation 6 | 7 | **Status**: ✅ Production Ready | **Version**: 1.0.0 | **Tests**: 27/27 Passing 8 | 9 | ### Quick Navigation 10 | 11 | - **[📌 Start Here](./index.md)** - Main landing page with overview 12 | - **[🚀 Quick Start Guide](./guide.md)** - Get up and running in minutes 13 | - **[📡 API Reference](./api.md)** - Complete REST API documentation 14 | - **[🏗️ Architecture](./architecture.md)** - System design and components 15 | - **[📝 Changelog](./CHANGELOG.md)** - Version history and updates 16 | 17 | --- 18 | 19 | ## 📚 Documentation Sections 20 | 21 | ### For New Users 22 | Start with these if you're new to CertMate Client Certificates: 23 | 24 | 1. **[Getting Started](./guide.md#getting-started)** 25 | - Installation and setup 26 | - First certificate creation 27 | - Web dashboard tour 28 | 29 | 2. **[Common Tasks](./guide.md#common-tasks)** 30 | - Creating certificates 31 | - Batch importing 32 | - Downloading files 33 | - Renewing and revoking 34 | 35 | ### For Developers 36 | Use these if you're integrating with the API: 37 | 38 | 1. **[API Reference](./api.md)** 39 | - All endpoints documented 40 | - Request/response examples 41 | - Error handling 42 | - Rate limiting info 43 | 44 | 2. **[Architecture](./architecture.md)** 45 | - System components 46 | - Data flow 47 | - Security model 48 | - Scalability design 49 | 50 | ### For Administrators 51 | Use these to manage and monitor the system: 52 | 53 | 1. **[Audit Logging](./api.md#audit-logging)** 54 | - How to access audit logs 55 | - Understanding log entries 56 | 57 | 2. **[Rate Limiting](./api.md#rate-limiting)** 58 | - Default limits 59 | - Configuration 60 | - Per-endpoint limits 61 | 62 | --- 63 | 64 | ## 🎯 Feature Overview 65 | 66 | ### Phase 1: CA Foundation ✅ 67 | - Self-signed Certificate Authority (4096-bit RSA) 68 | - CSR validation and creation 69 | - Secure key storage 70 | 71 | ### Phase 2: Client Certificate Engine ✅ 72 | - Complete lifecycle management 73 | - Multi-filter queries and search 74 | - Auto-renewal scheduling 75 | - Support for 30k+ certificates 76 | 77 | ### Phase 3: UI & Advanced Features ✅ 78 | - Web dashboard at `/client-certificates` 79 | - OCSP real-time status queries 80 | - CRL generation and distribution 81 | - REST API (10 endpoints) 82 | - Batch CSV import 83 | 84 | ### Phase 4: Easy Wins ✅ 85 | - Comprehensive audit logging 86 | - API rate limiting 87 | - Production-ready security 88 | 89 | --- 90 | 91 | ## 🔗 API Endpoints Quick Reference 92 | 93 | | Method | Endpoint | Description | 94 | |--------|----------|-------------| 95 | | POST | `/api/client-certs/create` | Create certificate | 96 | | GET | `/api/client-certs` | List certificates | 97 | | GET | `/api/client-certs/` | Get metadata | 98 | | GET | `/api/client-certs//download/` | Download cert/key/csr | 99 | | POST | `/api/client-certs//revoke` | Revoke certificate | 100 | | POST | `/api/client-certs//renew` | Renew certificate | 101 | | GET | `/api/client-certs/stats` | Get statistics | 102 | | POST | `/api/client-certs/batch` | Batch CSV import | 103 | | GET | `/api/ocsp/status/` | OCSP status | 104 | | GET | `/api/crl/download/` | Download CRL | 105 | 106 | See [API Reference](./api.md#endpoints) for full documentation. 107 | 108 | --- 109 | 110 | ## 🧪 Testing 111 | 112 | All features are thoroughly tested: 113 | 114 | ```bash 115 | # Run E2E tests 116 | python test_e2e_complete.py 117 | 118 | # Result: 27/27 tests passing 119 | ``` 120 | 121 | Test coverage includes: 122 | - ✅ CA Operations 123 | - ✅ CSR Operations 124 | - ✅ Certificate Lifecycle 125 | - ✅ Filtering & Search 126 | - ✅ Batch Operations 127 | - ✅ OCSP & CRL 128 | - ✅ Audit & Rate Limiting 129 | 130 | --- 131 | 132 | ## 🔐 Security Features 133 | 134 | - **4096-bit RSA** for CA keys 135 | - **SHA256** signature algorithm 136 | - **Bearer token** authentication 137 | - **Rate limiting** on all endpoints 138 | - **Audit logging** of all operations 139 | - **File permissions** 0600 for private keys 140 | 141 | --- 142 | 143 | ## 📊 Performance 144 | 145 | - Supports **30k+ concurrent certificates** 146 | - Efficient **multi-filter queries** 147 | - **Auto-renewal** scheduling 148 | - **Batch operations** with error tracking 149 | 150 | --- 151 | 152 | ## 🆘 Need Help? 153 | 154 | 1. **Installation Issues?** → See [Installation Section](./guide.md#installation) 155 | 2. **API Questions?** → See [API Reference](./api.md) 156 | 3. **Architecture Questions?** → See [Architecture Doc](./architecture.md) 157 | 4. **Something Else?** → Check the [Changelog](./CHANGELOG.md) 158 | 159 | --- 160 | 161 | ## 📝 File Structure 162 | 163 | ``` 164 | docs/ 165 | ├── README.md ← You are here 166 | ├── index.md ← Main landing page 167 | ├── guide.md ← User guide & getting started 168 | ├── api.md ← Complete API reference 169 | ├── architecture.md ← System design & components 170 | └── CHANGELOG.md ← Version history 171 | ``` 172 | 173 | --- 174 | 175 | ## 🎓 Learning Path 176 | 177 | **Beginner** → [Start Here](./index.md) → [Getting Started](./guide.md) 178 | 179 | **Developer** → [API Reference](./api.md) → [Architecture](./architecture.md) 180 | 181 | **Advanced** → [Full API Docs](./api.md) → [Architecture Details](./architecture.md) 182 | 183 | --- 184 | 185 | ## 📌 Important Links 186 | 187 | - **Web Dashboard**: `http://localhost:5000/client-certificates` 188 | - **API Docs**: `http://localhost:5000/docs/` 189 | - **Health Check**: `http://localhost:5000/health` 190 | - **Audit Logs**: `logs/audit/certificate_audit.log` 191 | 192 | --- 193 | 194 | ## 📊 Status Dashboard 195 | 196 | | Component | Status | Tests | 197 | |-----------|--------|-------| 198 | | CA Foundation | ✅ Ready | 3/3 | 199 | | CSR Handler | ✅ Ready | 3/3 | 200 | | Cert Manager | ✅ Ready | 8/8 | 201 | | Filtering | ✅ Ready | 3/3 | 202 | | Batch Ops | ✅ Ready | 2/2 | 203 | | OCSP/CRL | ✅ Ready | 5/5 | 204 | | Audit/Rate Limit | ✅ Ready | 3/3 | 205 | | **Total** | **✅ Ready** | **27/27** | 206 | 207 | --- 208 | 209 | ## 💡 Quick Examples 210 | 211 | ### Create a Certificate via API 212 | 213 | ```bash 214 | curl -X POST http://localhost:5000/api/client-certs/create \ 215 | -H "Authorization: Bearer YOUR_TOKEN" \ 216 | -H "Content-Type: application/json" \ 217 | -d '{ 218 | "common_name": "user@example.com", 219 | "organization": "ACME Corp", 220 | "cert_usage": "api-mtls", 221 | "days_valid": 365 222 | }' 223 | ``` 224 | 225 | ### List Certificates 226 | 227 | ```bash 228 | curl http://localhost:5000/api/client-certs \ 229 | -H "Authorization: Bearer YOUR_TOKEN" 230 | ``` 231 | 232 | ### Download Certificate 233 | 234 | ```bash 235 | curl http://localhost:5000/api/client-certs/USER_ID/download/crt \ 236 | -H "Authorization: Bearer YOUR_TOKEN" \ 237 | -o certificate.crt 238 | ``` 239 | 240 | See [API Guide](./api.md) for more examples. 241 | 242 | --- 243 | 244 | ## 📄 License 245 | 246 | CertMate is licensed under the MIT License. See LICENSE file in the repository. 247 | 248 | --- 249 | 250 | ## 🙋 Questions or Issues? 251 | 252 | - Check the relevant documentation page 253 | - Review the test files for usage examples 254 | - Check the [API Reference](./api.md) for endpoint details 255 | 256 | --- 257 | 258 |
259 | 260 | **Made with ❤️ for CertMate** 261 | 262 | [Home](../README.md) • [Documentation](./) • [GitHub](https://github.com/fabriziosalmi/certmate) 263 | 264 |
265 | -------------------------------------------------------------------------------- /test_domain_alias.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test domain alias functionality for DNS challenges 3 | """ 4 | import pytest 5 | import tempfile 6 | from pathlib import Path 7 | from modules.core.shell import MockShellExecutor 8 | from modules.core.certificates import CertificateManager 9 | from modules.core.settings import SettingsManager 10 | from modules.core.dns_providers import DNSManager 11 | from modules.core.file_operations import FileOperations 12 | from modules.core.dns_strategies import DNSStrategyFactory, CloudflareStrategy 13 | 14 | 15 | def test_domain_alias_in_certbot_command(): 16 | """Test that domain_alias is correctly added to certbot command""" 17 | strategy = CloudflareStrategy() 18 | cmd = ['certbot', 'certonly'] 19 | credentials_file = Path('/tmp/test_creds') 20 | domain_alias = '_acme-challenge.validation.example.org' 21 | 22 | strategy.configure_certbot_arguments(cmd, credentials_file, domain_alias=domain_alias) 23 | 24 | # Verify domain-alias flag is in command 25 | assert '--domain-alias' in cmd 26 | alias_index = cmd.index('--domain-alias') 27 | assert cmd[alias_index + 1] == domain_alias 28 | 29 | 30 | def test_domain_alias_optional(): 31 | """Test that domain_alias is optional and doesn't break existing functionality""" 32 | strategy = CloudflareStrategy() 33 | cmd = ['certbot', 'certonly'] 34 | credentials_file = Path('/tmp/test_creds') 35 | 36 | # Call without domain_alias 37 | strategy.configure_certbot_arguments(cmd, credentials_file) 38 | 39 | # Verify domain-alias flag is NOT in command 40 | assert '--domain-alias' not in cmd 41 | 42 | 43 | def test_certificate_manager_with_domain_alias(): 44 | """Test CertificateManager accepts and passes domain_alias""" 45 | with tempfile.TemporaryDirectory() as tmpdir: 46 | tmppath = Path(tmpdir) 47 | cert_dir = tmppath / "certs" 48 | data_dir = tmppath / "data" 49 | backup_dir = tmppath / "backups" 50 | logs_dir = tmppath / "logs" 51 | 52 | for d in [cert_dir, data_dir, backup_dir, logs_dir]: 53 | d.mkdir(parents=True, exist_ok=True) 54 | 55 | file_ops = FileOperations(cert_dir, data_dir, backup_dir, logs_dir) 56 | settings_file = data_dir / "settings.json" 57 | settings_manager = SettingsManager(file_ops, settings_file) 58 | dns_manager = DNSManager(settings_manager) 59 | 60 | # Save minimal settings 61 | settings_manager.save_settings({ 62 | 'email': 'test@example.com', 63 | 'dns_provider': 'cloudflare', 64 | 'dns_providers': { 65 | 'cloudflare': { 66 | 'accounts': { 67 | 'default': { 68 | 'api_token': 'test_token_1234567890abcdef' 69 | } 70 | } 71 | } 72 | } 73 | }) 74 | 75 | mock_executor = MockShellExecutor() 76 | # Mock successful certbot run 77 | mock_executor.set_next_result(returncode=0, stdout="Certificate created successfully") 78 | 79 | cert_manager = CertificateManager( 80 | cert_dir=cert_dir, 81 | settings_manager=settings_manager, 82 | dns_manager=dns_manager, 83 | shell_executor=mock_executor 84 | ) 85 | 86 | # Try to create a certificate with domain_alias 87 | try: 88 | result = cert_manager.create_certificate( 89 | domain='test.example.com', 90 | email='test@example.com', 91 | dns_provider='cloudflare', 92 | dns_config={'api_token': 'test_token_1234567890abcdef'}, 93 | domain_alias='_acme-challenge.validation.example.org' 94 | ) 95 | # If we get here, the mock was used successfully 96 | assert result['success'] is True 97 | except Exception as e: 98 | # Expected to fail due to missing cert files, but executor was called 99 | assert mock_executor.call_count > 0 100 | 101 | # Verify the command included domain-alias 102 | if mock_executor.commands_executed: 103 | cmd_str = mock_executor.commands_executed[0] 104 | assert '--domain-alias' in cmd_str 105 | assert '_acme-challenge.validation.example.org' in cmd_str 106 | 107 | 108 | def test_certificate_manager_without_domain_alias(): 109 | """Test backward compatibility - certificate creation without domain_alias""" 110 | with tempfile.TemporaryDirectory() as tmpdir: 111 | tmppath = Path(tmpdir) 112 | cert_dir = tmppath / "certs" 113 | data_dir = tmppath / "data" 114 | backup_dir = tmppath / "backups" 115 | logs_dir = tmppath / "logs" 116 | 117 | for d in [cert_dir, data_dir, backup_dir, logs_dir]: 118 | d.mkdir(parents=True, exist_ok=True) 119 | 120 | file_ops = FileOperations(cert_dir, data_dir, backup_dir, logs_dir) 121 | settings_file = data_dir / "settings.json" 122 | settings_manager = SettingsManager(file_ops, settings_file) 123 | dns_manager = DNSManager(settings_manager) 124 | 125 | settings_manager.save_settings({ 126 | 'email': 'test@example.com', 127 | 'dns_provider': 'cloudflare', 128 | 'dns_providers': { 129 | 'cloudflare': { 130 | 'accounts': { 131 | 'default': { 132 | 'api_token': 'test_token_1234567890abcdef' 133 | } 134 | } 135 | } 136 | } 137 | }) 138 | 139 | mock_executor = MockShellExecutor() 140 | mock_executor.set_next_result(returncode=0, stdout="Certificate created successfully") 141 | 142 | cert_manager = CertificateManager( 143 | cert_dir=cert_dir, 144 | settings_manager=settings_manager, 145 | dns_manager=dns_manager, 146 | shell_executor=mock_executor 147 | ) 148 | 149 | # Create certificate WITHOUT domain_alias 150 | try: 151 | result = cert_manager.create_certificate( 152 | domain='test.example.com', 153 | email='test@example.com', 154 | dns_provider='cloudflare', 155 | dns_config={'api_token': 'test_token_1234567890abcdef'} 156 | # No domain_alias parameter 157 | ) 158 | assert result['success'] is True 159 | except Exception: 160 | assert mock_executor.call_count > 0 161 | 162 | # Verify the command does NOT include domain-alias 163 | if mock_executor.commands_executed: 164 | cmd_str = mock_executor.commands_executed[0] 165 | assert '--domain-alias' not in cmd_str 166 | 167 | 168 | def test_all_strategies_support_domain_alias(): 169 | """Test that all DNS strategies support domain_alias parameter""" 170 | strategies = [ 171 | 'cloudflare', 'route53', 'azure', 'google', 'powerdns', 172 | 'digitalocean', 'linode', 'gandi', 'ovh', 'namecheap', 173 | 'hetzner', 'porkbun', 'godaddy', 'arvancloud', 'acme-dns' 174 | ] 175 | 176 | for strategy_name in strategies: 177 | strategy = DNSStrategyFactory.get_strategy(strategy_name) 178 | cmd = ['certbot', 'certonly'] 179 | credentials_file = Path('/tmp/test') 180 | 181 | # Should not raise an error 182 | try: 183 | strategy.configure_certbot_arguments( 184 | cmd, credentials_file, 185 | domain_alias='_acme-challenge.validation.example.org' 186 | ) 187 | # Verify domain-alias was added 188 | assert '--domain-alias' in cmd 189 | except TypeError as e: 190 | pytest.fail(f"Strategy {strategy_name} doesn't support domain_alias: {e}") 191 | -------------------------------------------------------------------------------- /DOCKER_MULTIPLATFORM.md: -------------------------------------------------------------------------------- 1 | # 🐳 Docker Multi-Platform Guide 2 | 3 | This guide explains how to build and use CertMate Docker images that work on both ARM and non-ARM architectures. 4 | 5 | ## 🎯 Quick Start 6 | 7 | ### For Users (Pull and Run) 8 | 9 | The easiest way to use CertMate is to pull the pre-built multi-platform image: 10 | 11 | ```bash 12 | # Pull and run - Docker automatically selects the right architecture 13 | docker run -d --name certmate \ 14 | --env-file .env \ 15 | -p 8000:8000 \ 16 | -v certmate_data:/app/data \ 17 | USERNAME/certmate:latest 18 | ``` 19 | 20 | ### For Developers (Build Multi-Platform) 21 | 22 | #### Option 1: Use the Enhanced Build Script 23 | 24 | ```bash 25 | # Build for current platform only (fastest) 26 | ./build-docker.sh 27 | 28 | # Build for multiple platforms (ARM64 + AMD64) 29 | ./build-docker.sh -m 30 | 31 | # Build and push to Docker Hub 32 | ./build-docker.sh -m -p -r YOUR_DOCKERHUB_USERNAME 33 | 34 | # Build with custom platforms 35 | ./build-docker.sh --platforms linux/amd64,linux/arm64,linux/arm/v7 -m -p -r USERNAME 36 | ``` 37 | 38 | #### Option 2: Use the Dedicated Multi-Platform Script 39 | 40 | ```bash 41 | # Make executable (first time only) 42 | chmod +x build-multiplatform.sh 43 | 44 | # Build locally for AMD64 and ARM64 45 | ./build-multiplatform.sh 46 | 47 | # Build and push to Docker Hub 48 | ./build-multiplatform.sh -r YOUR_DOCKERHUB_USERNAME -p 49 | 50 | # Build specific version 51 | ./build-multiplatform.sh -r USERNAME -v v1.0.0 -p 52 | 53 | # Build for Raspberry Pi (ARM v7) 54 | ./build-multiplatform.sh --platforms linux/arm/v7 -r USERNAME -p 55 | ``` 56 | 57 | #### Option 3: Manual Docker Buildx Commands 58 | 59 | ```bash 60 | # Create and use buildx builder 61 | docker buildx create --name certmate-builder --use 62 | 63 | # Build for multiple platforms 64 | docker buildx build --platform linux/amd64,linux/arm64 -t USERNAME/certmate:latest . 65 | 66 | # Build and push 67 | docker buildx build --platform linux/amd64,linux/arm64 -t USERNAME/certmate:latest --push . 68 | ``` 69 | 70 | ## 🏗️ Supported Architectures 71 | 72 | | Platform | Description | Common Use Cases | 73 | |----------|-------------|------------------| 74 | | `linux/amd64` | Intel/AMD 64-bit | Most cloud servers, desktops, laptops | 75 | | `linux/arm64` | ARM 64-bit | Apple Silicon Macs, ARM cloud instances, modern ARM servers | 76 | | `linux/arm/v7` | ARM 32-bit v7 | Raspberry Pi 3+, some IoT devices | 77 | | `linux/arm/v6` | ARM 32-bit v6 | Raspberry Pi 1, Zero | 78 | 79 | ## 🔧 Setup Instructions 80 | 81 | ### Prerequisites 82 | 83 | 1. **Docker with Buildx** (Docker Desktop includes this) 84 | 2. **Multi-platform emulation** (QEMU, included in Docker Desktop) 85 | 86 | ### Verify Buildx Support 87 | 88 | ```bash 89 | # Check if buildx is available 90 | docker buildx version 91 | 92 | # List available builders 93 | docker buildx ls 94 | 95 | # Check supported platforms 96 | docker buildx inspect --bootstrap 97 | ``` 98 | 99 | ### Enable Multi-Platform Emulation 100 | 101 | If you see limited platforms, enable emulation: 102 | 103 | ```bash 104 | # Install QEMU emulators 105 | docker run --privileged --rm tonistiigi/binfmt --install all 106 | 107 | # Verify available platforms 108 | docker buildx inspect --bootstrap 109 | ``` 110 | 111 | ## 🚀 Usage Examples 112 | 113 | ### Running on Specific Platforms 114 | 115 | ```bash 116 | # Force AMD64 (useful for performance testing) 117 | docker run --platform linux/amd64 --rm \ 118 | --env-file .env -p 8000:8000 USERNAME/certmate:latest 119 | 120 | # Force ARM64 (useful on Apple Silicon) 121 | docker run --platform linux/arm64 --rm \ 122 | --env-file .env -p 8000:8000 USERNAME/certmate:latest 123 | 124 | # Auto-detect platform (recommended) 125 | docker run --rm --env-file .env -p 8000:8000 USERNAME/certmate:latest 126 | ``` 127 | 128 | ### Docker Compose Multi-Platform 129 | 130 | Update your `docker-compose.yml` to specify platform if needed: 131 | 132 | ```yaml 133 | services: 134 | certmate: 135 | image: USERNAME/certmate:latest 136 | platform: linux/amd64 # Optional: force specific platform 137 | # ... rest of your config 138 | ``` 139 | 140 | ### Checking Image Platforms 141 | 142 | ```bash 143 | # View available architectures 144 | docker manifest inspect USERNAME/certmate:latest 145 | 146 | # Pull specific platform 147 | docker pull --platform linux/arm64 USERNAME/certmate:latest 148 | ``` 149 | 150 | ## 🔨 Building Custom Images 151 | 152 | ### Environment Variables for Builds 153 | 154 | ```bash 155 | # Set target platforms 156 | export DOCKER_BUILDKIT=1 157 | export BUILDX_PLATFORMS="linux/amd64,linux/arm64" 158 | 159 | # Build with environment 160 | docker buildx build --platform $BUILDX_PLATFORMS -t certmate:custom . 161 | ``` 162 | 163 | ### Build Arguments 164 | 165 | ```bash 166 | # Use different requirements file 167 | docker buildx build \ 168 | --platform linux/amd64,linux/arm64 \ 169 | --build-arg REQUIREMENTS_FILE=requirements.txt \ 170 | -t certmate:full . 171 | 172 | # Build with specific Python version 173 | docker buildx build \ 174 | --platform linux/amd64,linux/arm64 \ 175 | --build-arg PYTHON_VERSION=3.11 \ 176 | -t certmate:py311 . 177 | ``` 178 | 179 | ## 🐛 Troubleshooting 180 | 181 | ### Common Issues 182 | 183 | 1. **"multiple platforms feature is currently not supported for docker driver"** 184 | ```bash 185 | # Create a new builder 186 | docker buildx create --name multiplatform --use 187 | ``` 188 | 189 | 2. **"exec format error" when running** 190 | ```bash 191 | # Enable emulation 192 | docker run --privileged --rm tonistiigi/binfmt --install all 193 | ``` 194 | 195 | 3. **Slow builds on non-native platforms** 196 | - This is normal due to emulation 197 | - Use GitHub Actions for production builds 198 | - Consider building on native hardware for each platform 199 | 200 | 4. **Cannot load multi-platform image to local Docker** 201 | ```bash 202 | # Multi-platform images must be pushed to registry 203 | # For local testing, build single platform: 204 | docker buildx build --platform linux/amd64 --load -t certmate:test . 205 | ``` 206 | 207 | ### Performance Tips 208 | 209 | 1. **Use layer caching**: 210 | ```bash 211 | docker buildx build --cache-from type=registry,ref=USERNAME/certmate:cache . 212 | ``` 213 | 214 | 2. **Parallel builds**: Use GitHub Actions or multiple machines for faster builds 215 | 216 | 3. **Minimal base images**: The current Dockerfile already uses `python:3.11-slim` 217 | 218 | ## 🤖 CI/CD Integration 219 | 220 | ### GitHub Actions 221 | 222 | The repository includes `.github/workflows/docker-multiplatform.yml` for automated builds. 223 | 224 | Required secrets: 225 | - `DOCKERHUB_USERNAME`: Your Docker Hub username 226 | - `DOCKERHUB_TOKEN`: Docker Hub access token 227 | 228 | ### Manual Triggers 229 | 230 | ```bash 231 | # Trigger workflow with custom platforms 232 | gh workflow run docker-multiplatform.yml \ 233 | -f platforms="linux/amd64,linux/arm64,linux/arm/v7" \ 234 | -f push_to_registry=true 235 | ``` 236 | 237 | ## 📊 Image Information 238 | 239 | ### Size Comparison 240 | 241 | Typical image sizes: 242 | - AMD64: ~200-300 MB 243 | - ARM64: ~200-300 MB 244 | - ARM v7: ~180-250 MB 245 | 246 | ### Manifest Example 247 | 248 | ```json 249 | { 250 | "manifests": [ 251 | { 252 | "platform": { 253 | "architecture": "amd64", 254 | "os": "linux" 255 | } 256 | }, 257 | { 258 | "platform": { 259 | "architecture": "arm64", 260 | "os": "linux" 261 | } 262 | } 263 | ] 264 | } 265 | ``` 266 | 267 | ## 🔗 Useful Commands 268 | 269 | ```bash 270 | # Clean up builders 271 | docker buildx prune 272 | docker buildx rm certmate-builder 273 | 274 | # Inspect image details 275 | docker buildx imagetools inspect USERNAME/certmate:latest 276 | 277 | # Check platform of running container 278 | docker inspect CONTAINER_ID | grep Architecture 279 | 280 | # Force rebuild without cache 281 | docker buildx build --no-cache --platform linux/amd64,linux/arm64 . 282 | ``` 283 | 284 | ## 🎯 Best Practices 285 | 286 | 1. **Always test on target platforms** before releasing 287 | 2. **Use registry caching** for faster CI/CD builds 288 | 3. **Pin base image versions** for reproducible builds 289 | 4. **Monitor image sizes** across architectures 290 | 5. **Document platform-specific requirements** if any 291 | 6. **Use health checks** to verify container startup 292 | 7. **Test with different DNS providers** on each platform 293 | 294 | ## 📚 Additional Resources 295 | 296 | - [Docker Buildx Documentation](https://docs.docker.com/buildx/) 297 | - [Multi-platform Images](https://docs.docker.com/build/building/multi-platform/) 298 | - [GitHub Actions Docker Build](https://github.com/docker/build-push-action) 299 | - [Docker Hub Multi-arch](https://docs.docker.com/docker-hub/builds/advanced/) 300 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # CertMate - Client Certificates 2 | 3 |
4 | 5 | ![CertMate](https://img.shields.io/badge/CertMate-Client%20Certificates-blue?style=for-the-badge) 6 | ![Status](https://img.shields.io/badge/Status-Production%20Ready-green?style=for-the-badge) 7 | ![Tests](https://img.shields.io/badge/Tests-27%2F27%20Passing-brightgreen?style=for-the-badge) 8 | ![Coverage](https://img.shields.io/badge/Coverage-100%25-brightgreen?style=for-the-badge) 9 | 10 | **Complete Client Certificate Management for CertMate** 11 | 12 | [📖 Documentation](#documentation) • [🚀 Quick Start](#quick-start) • [📡 API Reference](./api.md) • [🏗️ Architecture](./architecture.md) 13 | 14 |
15 | 16 | --- 17 | 18 | ## Overview 19 | 20 | CertMate Client Certificates is a comprehensive, production-ready solution for managing client certificates with: 21 | 22 | - 🔐 **Self-Signed CA** - Generate and manage your own Certificate Authority 23 | - 🎫 **Full Lifecycle Management** - Create, renew, revoke, and monitor client certificates 24 | - 📊 **OCSP & CRL** - Real-time certificate status and revocation lists 25 | - 🌐 **Web Dashboard** - Intuitive UI for certificate management 26 | - 📡 **REST API** - Complete API for automation 27 | - 📦 **Batch Operations** - Import 100-30,000 certificates via CSV 28 | - 📋 **Audit Logging** - Track all operations for compliance 29 | - ⚡ **Rate Limiting** - Built-in protection against abuse 30 | 31 | --- 32 | 33 | ## Features 34 | 35 | ### Phase 1: CA Foundation ✅ 36 | - **PrivateCAGenerator**: Self-signed CA with 4096-bit RSA keys, 10-year validity 37 | - **CSRHandler**: Validate, create, and parse Certificate Signing Requests 38 | - **Secure Storage**: Proper file permissions (0600) for private keys 39 | 40 | ### Phase 2: Client Certificate Engine ✅ 41 | - **Complete Lifecycle**: Create, list, filter, revoke, and renew certificates 42 | - **Multi-Filter Queries**: Search by usage type, revocation status, common name 43 | - **Auto-Renewal**: Scheduled daily renewal checks for expiring certificates 44 | - **Support for 30k+ Certificates**: Directory-based storage for linear scalability 45 | - **Metadata Management**: Track CN, email, organization, usage, expiration dates 46 | 47 | ### Phase 3: UI & Advanced Features ✅ 48 | - **Web Dashboard**: Responsive, dark-mode-enabled management interface 49 | - **OCSP Responder**: Query certificate status in real-time 50 | - **CRL Manager**: Generate and distribute revocation lists (PEM/DER) 51 | - **REST API**: 10 endpoints across 3 namespaces for full automation 52 | - **Batch Operations**: Import certificates from CSV files 53 | 54 | ### Phase 4: Easy Wins ✅ 55 | - **Audit Logging**: Track all certificate operations with user/IP information 56 | - **Rate Limiting**: Configurable per-endpoint limits with sensible defaults 57 | - **Ready for Integration**: Both managers available in app for immediate use 58 | 59 | --- 60 | 61 | ## Quick Start 62 | 63 | ### Installation 64 | 65 | ```bash 66 | # Install dependencies 67 | pip install -r requirements.txt 68 | 69 | # Run the application 70 | python app.py 71 | ``` 72 | 73 | The server will start on `http://localhost:5000` 74 | 75 | ### Basic Usage 76 | 77 | #### 1. Access Web Dashboard 78 | ``` 79 | Navigate to: http://localhost:5000/client-certificates 80 | ``` 81 | 82 | #### 2. Create a Certificate via API 83 | ```bash 84 | curl -X POST http://localhost:5000/api/client-certs/create \ 85 | -H "Authorization: Bearer YOUR_TOKEN" \ 86 | -H "Content-Type: application/json" \ 87 | -d '{ 88 | "common_name": "user@example.com", 89 | "email": "user@example.com", 90 | "organization": "ACME Corp", 91 | "cert_usage": "api-mtls", 92 | "days_valid": 365, 93 | "generate_key": true 94 | }' 95 | ``` 96 | 97 | #### 3. List Certificates 98 | ```bash 99 | curl http://localhost:5000/api/client-certs \ 100 | -H "Authorization: Bearer YOUR_TOKEN" 101 | ``` 102 | 103 | #### 4. Download Certificate Files 104 | ```bash 105 | # Download certificate 106 | curl http://localhost:5000/api/client-certs/USER_ID/download/crt \ 107 | -H "Authorization: Bearer YOUR_TOKEN" \ 108 | -o user.crt 109 | 110 | # Download private key 111 | curl http://localhost:5000/api/client-certs/USER_ID/download/key \ 112 | -H "Authorization: Bearer YOUR_TOKEN" \ 113 | -o user.key 114 | ``` 115 | 116 | --- 117 | 118 | ## Documentation 119 | 120 | ### 📖 Main Documentation 121 | 122 | - **[API Reference](./api.md)** - Complete REST API documentation with examples 123 | - **[Architecture](./architecture.md)** - System design, components, and data flow 124 | - **[User Guide](./guide.md)** - Step-by-step guide for common tasks 125 | - **[Changelog](./CHANGELOG.md)** - Version history and updates 126 | 127 | ### 🔗 Quick Links 128 | 129 | - [API Endpoints](./api.md#endpoints) - All available endpoints 130 | - [Certificate Types](./api.md#certificate-types) - VPN, API mTLS, etc. 131 | - [Rate Limiting](./api.md#rate-limiting) - Default limits and configuration 132 | - [Audit Logging](./api.md#audit-logging) - Understanding audit trails 133 | 134 | --- 135 | 136 | ## Testing 137 | 138 | All features have been extensively tested: 139 | 140 | ```bash 141 | # Run comprehensive test suite 142 | python test_e2e_complete.py 143 | 144 | # Expected result: 27/27 tests passing 145 | ``` 146 | 147 | ### Test Coverage 148 | - ✅ CA Operations (3 tests) 149 | - ✅ CSR Operations (3 tests) 150 | - ✅ Certificate Lifecycle (8 tests) 151 | - ✅ Filtering & Search (3 tests) 152 | - ✅ Batch Operations (2 tests) 153 | - ✅ OCSP & CRL (5 tests) 154 | - ✅ Audit & Rate Limiting (3 tests) 155 | 156 | --- 157 | 158 | ## API Endpoints Summary 159 | 160 | | Method | Endpoint | Purpose | 161 | |--------|----------|---------| 162 | | `POST` | `/api/client-certs/create` | Create new certificate | 163 | | `GET` | `/api/client-certs` | List certificates with filters | 164 | | `GET` | `/api/client-certs/` | Get certificate metadata | 165 | | `GET` | `/api/client-certs//download/` | Download cert/key/csr | 166 | | `POST` | `/api/client-certs//revoke` | Revoke certificate | 167 | | `POST` | `/api/client-certs//renew` | Renew certificate | 168 | | `GET` | `/api/client-certs/stats` | Get statistics | 169 | | `POST` | `/api/client-certs/batch` | Batch CSV import | 170 | | `GET` | `/api/ocsp/status/` | OCSP status query | 171 | | `GET` | `/api/crl/download/` | Download CRL (PEM/DER) | 172 | 173 | --- 174 | 175 | ## Architecture 176 | 177 | The system is built with a modular, layered architecture: 178 | 179 | ``` 180 | ┌─────────────────────────────────────────┐ 181 | │ Web UI & REST API │ 182 | │ (/client-certificates, /api/*) │ 183 | ├─────────────────────────────────────────┤ 184 | │ API Resources & Managers │ 185 | │ (OCSP, CRL, Audit, Rate Limiting) │ 186 | ├─────────────────────────────────────────┤ 187 | │ Core Modules │ 188 | │ (Certificate Mgmt, CSR, CA, Storage) │ 189 | ├─────────────────────────────────────────┤ 190 | │ Cryptography & Storage │ 191 | │ (OpenSSL, File System, Backends) │ 192 | └─────────────────────────────────────────┘ 193 | ``` 194 | 195 | See [Architecture Documentation](./architecture.md) for detailed information. 196 | 197 | --- 198 | 199 | ## Security 200 | 201 | ### Cryptographic Strength 202 | - **CA**: 4096-bit RSA keys, 10-year validity 203 | - **Client Certificates**: 2048 or 4096-bit RSA (configurable) 204 | - **Signatures**: SHA256 205 | - **Key Storage**: 0600 file permissions on Unix systems 206 | 207 | ### Access Control 208 | - **Bearer Token Authentication** on all API endpoints 209 | - **Rate Limiting**: Per-endpoint configurable limits 210 | - **Audit Logging**: All operations tracked with user/IP info 211 | 212 | ### Compliance 213 | - ✅ Certificate metadata tracking 214 | - ✅ Revocation audit trail 215 | - ✅ Persistent operation logs 216 | - ✅ Support for compliance queries 217 | 218 | --- 219 | 220 | ## Performance 221 | 222 | The implementation is optimized for: 223 | - **Scalability**: Directory-based storage supports 30k+ concurrent certificates 224 | - **Speed**: Efficient multi-filter queries 225 | - **Reliability**: Automatic renewal scheduling 226 | - **Responsiveness**: Async JavaScript in web UI 227 | 228 | --- 229 | 230 | ## Support 231 | 232 | For questions or issues: 233 | 1. Check the [User Guide](./guide.md) 234 | 2. Review the [API Documentation](./api.md) 235 | 3. Check the [Architecture](./architecture.md) section 236 | 4. Review test cases in `test_e2e_complete.py` 237 | 238 | --- 239 | 240 | ## License 241 | 242 | See LICENSE file in the repository 243 | 244 | --- 245 | 246 | ## Version 247 | 248 | **Current Version**: 1.0.0 249 | **Status**: Production Ready 250 | **Last Updated**: 2024-10-30 251 | 252 | --- 253 | 254 |
255 | 256 | Made with ❤️ for CertMate 257 | 258 | [📄 Documentation](.) • [🔐 Privacy](./privacy.md) • [📜 License](../LICENSE) 259 | 260 |
261 | -------------------------------------------------------------------------------- /CA_PROVIDERS.md: -------------------------------------------------------------------------------- 1 | # Certificate Authority (CA) Providers in CertMate 2 | 3 | CertMate now supports multiple Certificate Authority providers, allowing you to choose the most appropriate CA for your needs, from free automated certificates to enterprise-grade solutions. 4 | 5 | ## Supported CA Providers 6 | 7 | ### 1. Let's Encrypt (Default) 8 | - **Type**: Free, automated SSL certificates 9 | - **Certificate Types**: Domain Validation (DV) 10 | - **Wildcard Support**: Yes 11 | - **EAB Required**: No 12 | - **Best For**: Development, small businesses, personal projects 13 | 14 | **Configuration:** 15 | - **Environment**: Choose between Production and Staging 16 | - **Email**: Required for certificate notifications 17 | 18 | ### 2. DigiCert ACME 19 | - **Type**: Enterprise-grade SSL certificates 20 | - **Certificate Types**: DV, OV, EV 21 | - **Wildcard Support**: Yes 22 | - **EAB Required**: Yes 23 | - **Best For**: Enterprise environments, commercial applications 24 | 25 | **Configuration Requirements:** 26 | - **ACME Directory URL**: Usually `https://acme.digicert.com/v2/acme/directory` 27 | - **EAB Key ID**: Provided by DigiCert 28 | - **EAB HMAC Key**: Provided by DigiCert 29 | - **Email**: Required for certificate notifications 30 | 31 | **Note**: You must have a DigiCert account and obtain EAB credentials before using this CA. 32 | 33 | ### 3. Private CA 34 | - **Type**: Internal/Corporate Certificate Authority 35 | - **Certificate Types**: Private/Internal 36 | - **Wildcard Support**: Yes (depends on CA implementation) 37 | - **EAB Required**: Optional (depends on CA configuration) 38 | - **Best For**: Internal networks, corporate environments, air-gapped systems 39 | 40 | **Configuration:** 41 | - **ACME Directory URL**: Your private CA's ACME endpoint 42 | - **CA Certificate**: Optional PEM-formatted root CA certificate 43 | - **EAB Credentials**: Optional, if required by your CA 44 | - **Email**: Required for certificate notifications 45 | 46 | **Compatible with:** 47 | - [step-ca](https://smallstep.com/docs/step-ca/) 48 | - [Boulder](https://github.com/letsencrypt/boulder) 49 | - [Pebble](https://github.com/letsencrypt/pebble) 50 | - Other ACME-compatible private CAs 51 | 52 | ## Configuration 53 | 54 | ### Via Web Interface 55 | 56 | 1. Navigate to **Settings** in the CertMate web interface 57 | 2. Scroll to the **Certificate Authority (CA) Providers** section 58 | 3. Select your default CA provider from the dropdown 59 | 4. Configure the settings for your chosen CA provider 60 | 5. Click **Test CA Connection** to verify the configuration 61 | 6. Save your settings 62 | 63 | ### Default CA Provider 64 | 65 | You can set a default CA provider that will be used for all new certificate requests. This can be overridden on a per-certificate basis during creation. 66 | 67 | ### Per-Certificate CA Selection 68 | 69 | When creating a new certificate, you can optionally select a different CA provider from the default: 70 | 71 | 1. Go to the main **Certificates** page 72 | 2. In the **Create New Certificate** form 73 | 3. Select your desired CA from the **Certificate Authority** dropdown 74 | 4. Proceed with certificate creation 75 | 76 | ## External Account Binding (EAB) 77 | 78 | Some CA providers (like DigiCert) require External Account Binding for security and account verification. 79 | 80 | ### What is EAB? 81 | EAB is a mechanism that links your ACME client to your account with the CA provider. It consists of: 82 | - **Key ID**: A unique identifier for your account 83 | - **HMAC Key**: A secret key used to sign requests 84 | 85 | ### Obtaining EAB Credentials 86 | 87 | #### DigiCert 88 | 1. Log into your DigiCert account 89 | 2. Navigate to the ACME settings 90 | 3. Generate or retrieve your EAB Key ID and HMAC Key 91 | 4. Enter these credentials in CertMate's DigiCert configuration 92 | 93 | #### Private CA 94 | EAB requirements vary by implementation: 95 | - **step-ca**: EAB can be enabled/disabled per need 96 | - **Boulder**: Typically requires EAB for production use 97 | - Check your private CA documentation for specific requirements 98 | 99 | ## SSL Certificate Trust 100 | 101 | ### Public CAs (Let's Encrypt, DigiCert) 102 | Certificates from public CAs are automatically trusted by browsers and operating systems. 103 | 104 | ### Private CAs 105 | For private CA certificates to be trusted: 106 | 107 | 1. **Install Root CA Certificate**: Install your private CA's root certificate on client systems 108 | 2. **Configure Applications**: Some applications may need specific trust configuration 109 | 3. **Browser Trust**: Import the root certificate into browser trust stores 110 | 111 | ### CA Certificate in CertMate 112 | 113 | For private CAs, you can optionally provide the root CA certificate in CertMate: 114 | - Helps with validation during certificate creation 115 | - Used for trust chain verification 116 | - Should be in PEM format 117 | 118 | ## Troubleshooting 119 | 120 | ### Connection Test Failures 121 | 122 | #### Let's Encrypt 123 | - **Staging URL accessible**: Verify internet connectivity 124 | - **Email valid**: Ensure email format is correct 125 | 126 | #### DigiCert 127 | - **Invalid EAB credentials**: Verify Key ID and HMAC Key from DigiCert account 128 | - **Account not authorized**: Ensure your DigiCert account has ACME enabled 129 | - **Wrong ACME URL**: Verify the directory URL with DigiCert support 130 | 131 | #### Private CA 132 | - **ACME URL unreachable**: Check network connectivity to your private CA 133 | - **CA certificate invalid**: Verify PEM format and certificate validity 134 | - **EAB mismatch**: Check if EAB is required and credentials are correct 135 | 136 | ### Certificate Creation Issues 137 | 138 | #### General 139 | - Ensure DNS provider is configured correctly 140 | - Verify domain ownership and DNS propagation 141 | - Check that the selected CA supports your domain type 142 | 143 | #### Private CA Specific 144 | - Verify your private CA is running and accessible 145 | - Check firewall rules for ACME port (usually 443) 146 | - Ensure CA has proper certificate chain configuration 147 | 148 | ## Security Considerations 149 | 150 | ### Credential Storage 151 | - EAB HMAC keys are not displayed after saving for security 152 | - Private keys are generated locally and never transmitted 153 | - Use secure storage backends for production environments 154 | 155 | ### CA Trust 156 | - Only use trusted CA providers 157 | - Verify CA certificates and EAB credentials through official channels 158 | - Monitor certificate transparency logs for unauthorized certificates 159 | 160 | ### Network Security 161 | - Use HTTPS for all CA communications 162 | - Consider VPN or private networks for private CA access 163 | - Implement proper firewall rules for CA connectivity 164 | 165 | ## Migration Between CAs 166 | 167 | You can change your default CA provider at any time: 168 | 169 | 1. **New Certificates**: Will use the new default CA 170 | 2. **Existing Certificates**: Will continue using their original CA until renewal 171 | 3. **Forced Migration**: Manually renew certificates to switch to the new CA 172 | 173 | ### Best Practices for Migration 174 | - Test new CA configuration before making it default 175 | - Plan migration during maintenance windows 176 | - Keep backup of existing certificates during transition 177 | - Monitor certificate validity after migration 178 | 179 | ## API Usage 180 | 181 | ### Create Certificate with Specific CA 182 | ```bash 183 | curl -X POST http://localhost:8000/api/certificates/create \ 184 | -H "Authorization: Bearer YOUR_TOKEN" \ 185 | -H "Content-Type: application/json" \ 186 | -d '{ 187 | "domain": "example.com", 188 | "ca_provider": "digicert" 189 | }' 190 | ``` 191 | 192 | ### Test CA Provider Connection 193 | ```bash 194 | curl -X POST http://localhost:8000/api/test-ca-provider \ 195 | -H "Authorization: Bearer YOUR_TOKEN" \ 196 | -H "Content-Type: application/json" \ 197 | -d '{ 198 | "ca_provider": "digicert", 199 | "config": { 200 | "acme_url": "https://acme.digicert.com/v2/acme/directory", 201 | "eab_kid": "your_key_id", 202 | "eab_hmac": "your_hmac_key", 203 | "email": "admin@example.com" 204 | } 205 | }' 206 | ``` 207 | 208 | ## Support and Resources 209 | 210 | ### Let's Encrypt 211 | - [Documentation](https://letsencrypt.org/docs/) 212 | - [Rate Limits](https://letsencrypt.org/docs/rate-limits/) 213 | - [Staging Environment](https://letsencrypt.org/docs/staging-environment/) 214 | 215 | ### DigiCert 216 | - [ACME Documentation](https://docs.digicert.com/certificate-tools/acme-user-guide/) 217 | - [Account Setup](https://docs.digicert.com/certificate-tools/acme-user-guide/acme-account-setup/) 218 | - [Support Portal](https://www.digicert.com/support/) 219 | 220 | ### Private CA Solutions 221 | - [step-ca Documentation](https://smallstep.com/docs/step-ca/) 222 | - [Boulder Project](https://github.com/letsencrypt/boulder) 223 | - [Pebble Test Server](https://github.com/letsencrypt/pebble) 224 | 225 | ## Changelog 226 | 227 | ### Version 1.3.0 228 | - Added support for multiple CA providers 229 | - Implemented DigiCert ACME integration 230 | - Added Private CA support with custom trust bundles 231 | - Enhanced certificate creation with CA selection 232 | - Added CA provider testing and validation 233 | -------------------------------------------------------------------------------- /modules/core/ocsp_crl.py: -------------------------------------------------------------------------------- 1 | """ 2 | OCSP and CRL module for CertMate 3 | Handles OCSP responses and Certificate Revocation List generation/distribution 4 | """ 5 | 6 | import logging 7 | from pathlib import Path 8 | from datetime import datetime 9 | from typing import Optional, List 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class OCSPResponder: 15 | """ 16 | Basic OCSP (Online Certificate Status Protocol) responder. 17 | Provides certificate status information (good, revoked, unknown). 18 | """ 19 | 20 | def __init__(self, private_ca, client_cert_manager): 21 | """ 22 | Initialize OCSP Responder. 23 | 24 | Args: 25 | private_ca: PrivateCAGenerator instance 26 | client_cert_manager: ClientCertificateManager instance 27 | """ 28 | self.private_ca = private_ca 29 | self.client_cert_manager = client_cert_manager 30 | 31 | def get_cert_status(self, serial_number: int) -> dict: 32 | """ 33 | Get OCSP status for a certificate. 34 | 35 | Args: 36 | serial_number: Certificate serial number 37 | 38 | Returns: 39 | Dictionary with status information 40 | """ 41 | try: 42 | # Search through certificates 43 | certificates = self.client_cert_manager.list_client_certificates() 44 | 45 | for cert in certificates: 46 | if int(cert.get('serial_number', 0)) == serial_number: 47 | # Certificate found 48 | if cert.get('revoked'): 49 | return { 50 | 'serial_number': serial_number, 51 | 'status': 'revoked', 52 | 'revoked_at': cert.get('revoked_at'), 53 | 'reason': cert.get('reason_revoked', 'unspecified'), 54 | 'this_update': datetime.utcnow().isoformat(), 55 | 'next_update': None # OCSP responses are generated on-demand 56 | } 57 | else: 58 | return { 59 | 'serial_number': serial_number, 60 | 'status': 'good', 61 | 'this_update': datetime.utcnow().isoformat(), 62 | 'next_update': None 63 | } 64 | 65 | # Certificate not found 66 | return { 67 | 'serial_number': serial_number, 68 | 'status': 'unknown', 69 | 'this_update': datetime.utcnow().isoformat(), 70 | 'next_update': None 71 | } 72 | 73 | except Exception as e: 74 | logger.error(f"Error getting OCSP status: {str(e)}") 75 | return { 76 | 'serial_number': serial_number, 77 | 'status': 'unknown', 78 | 'error': str(e) 79 | } 80 | 81 | def generate_ocsp_response(self, cert_status: dict) -> dict: 82 | """ 83 | Generate an OCSP response for a certificate status. 84 | 85 | Args: 86 | cert_status: Certificate status dictionary 87 | 88 | Returns: 89 | OCSP response as dictionary (can be serialized to DER if needed) 90 | """ 91 | try: 92 | response = { 93 | 'response_status': 'successful', 94 | 'certificate_status': cert_status['status'], 95 | 'certificate_serial': cert_status['serial_number'], 96 | 'this_update': cert_status.get('this_update'), 97 | 'next_update': cert_status.get('next_update'), 98 | 'responder_name': 'CertMate OCSP Responder', 99 | } 100 | 101 | if cert_status['status'] == 'revoked': 102 | response['revocation_time'] = cert_status.get('revoked_at') 103 | response['revocation_reason'] = cert_status.get('reason', 'unspecified') 104 | 105 | logger.debug(f"Generated OCSP response for serial {cert_status['serial_number']}") 106 | return response 107 | 108 | except Exception as e: 109 | logger.error(f"Error generating OCSP response: {str(e)}") 110 | return { 111 | 'response_status': 'internal_error', 112 | 'error': str(e) 113 | } 114 | 115 | 116 | class CRLManager: 117 | """ 118 | Manages Certificate Revocation List (CRL) generation and distribution. 119 | """ 120 | 121 | def __init__(self, private_ca, client_cert_manager, crl_dir: Path): 122 | """ 123 | Initialize CRL Manager. 124 | 125 | Args: 126 | private_ca: PrivateCAGenerator instance 127 | client_cert_manager: ClientCertificateManager instance 128 | crl_dir: Directory to store CRL files 129 | """ 130 | self.private_ca = private_ca 131 | self.client_cert_manager = client_cert_manager 132 | self.crl_dir = Path(crl_dir) 133 | self.crl_dir.mkdir(parents=True, exist_ok=True) 134 | 135 | def get_revoked_serials(self) -> List[int]: 136 | """ 137 | Get list of revoked certificate serial numbers. 138 | 139 | Returns: 140 | List of serial numbers 141 | """ 142 | try: 143 | certificates = self.client_cert_manager.list_client_certificates(revoked=True) 144 | serials = [] 145 | 146 | for cert in certificates: 147 | try: 148 | serial = int(cert.get('serial_number', 0)) 149 | if serial > 0: 150 | serials.append(serial) 151 | except (ValueError, TypeError): 152 | continue 153 | 154 | logger.debug(f"Found {len(serials)} revoked certificates for CRL") 155 | return serials 156 | 157 | except Exception as e: 158 | logger.error(f"Error getting revoked serials: {str(e)}") 159 | return [] 160 | 161 | def update_crl(self) -> Optional[bytes]: 162 | """ 163 | Update and generate CRL. 164 | 165 | Returns: 166 | CRL as PEM bytes or None if error 167 | """ 168 | try: 169 | revoked_serials = self.get_revoked_serials() 170 | crl_pem = self.private_ca.generate_crl(revoked_serials) 171 | 172 | if crl_pem: 173 | logger.info(f"Updated CRL with {len(revoked_serials)} revoked certificates") 174 | return crl_pem 175 | else: 176 | logger.warning("Failed to generate CRL") 177 | return None 178 | 179 | except Exception as e: 180 | logger.error(f"Error updating CRL: {str(e)}") 181 | return None 182 | 183 | def get_crl_pem(self) -> Optional[bytes]: 184 | """ 185 | Get current CRL in PEM format. 186 | 187 | Returns: 188 | CRL as PEM bytes or None 189 | """ 190 | try: 191 | crl_pem = self.private_ca.get_crl_pem() 192 | if crl_pem: 193 | return crl_pem 194 | 195 | # If no CRL exists, generate one 196 | return self.update_crl() 197 | 198 | except Exception as e: 199 | logger.error(f"Error getting CRL: {str(e)}") 200 | return None 201 | 202 | def get_crl_der(self) -> Optional[bytes]: 203 | """ 204 | Get CRL in DER format (for binary distribution). 205 | 206 | Returns: 207 | CRL as DER bytes or None 208 | """ 209 | try: 210 | from cryptography.hazmat.primitives import serialization 211 | 212 | crl_pem = self.get_crl_pem() 213 | if not crl_pem: 214 | return None 215 | 216 | # Convert PEM to DER 217 | from cryptography import x509 218 | crl = x509.load_pem_x509_crl(crl_pem) 219 | 220 | if crl: 221 | return crl.public_bytes(serialization.Encoding.DER) 222 | 223 | return None 224 | 225 | except Exception as e: 226 | logger.error(f"Error converting CRL to DER: {str(e)}") 227 | return None 228 | 229 | def get_crl_info(self) -> dict: 230 | """ 231 | Get information about the current CRL. 232 | 233 | Returns: 234 | Dictionary with CRL information 235 | """ 236 | try: 237 | from cryptography import x509 238 | 239 | crl_pem = self.get_crl_pem() 240 | if not crl_pem: 241 | return {'status': 'no_crl', 'message': 'No CRL available'} 242 | 243 | crl = x509.load_pem_x509_crl(crl_pem) 244 | 245 | revoked_serials = self.get_revoked_serials() 246 | 247 | return { 248 | 'status': 'available', 249 | 'issuer': str(crl.issuer), 250 | 'last_update': crl.last_update_utc.isoformat() if crl.last_update_utc else None, 251 | 'next_update': crl.next_update_utc.isoformat() if crl.next_update_utc else None, 252 | 'revoked_count': len(revoked_serials), 253 | 'revoked_serials': revoked_serials 254 | } 255 | 256 | except Exception as e: 257 | logger.error(f"Error getting CRL info: {str(e)}") 258 | return {'status': 'error', 'error': str(e)} 259 | -------------------------------------------------------------------------------- /modules/core/audit.py: -------------------------------------------------------------------------------- 1 | """ 2 | Audit logging module for CertMate 3 | Tracks all certificate operations for compliance and debugging 4 | """ 5 | 6 | import logging 7 | import json 8 | from pathlib import Path 9 | from datetime import datetime 10 | from typing import Optional, Dict, Any 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class AuditLogger: 16 | """Centralized audit logging for certificate operations.""" 17 | 18 | def __init__(self, audit_log_dir: Path): 19 | """ 20 | Initialize Audit Logger. 21 | 22 | Args: 23 | audit_log_dir: Directory to store audit logs 24 | """ 25 | self.audit_log_dir = Path(audit_log_dir) 26 | self.audit_log_dir.mkdir(parents=True, exist_ok=True) 27 | self.audit_log_file = self.audit_log_dir / "certificate_audit.log" 28 | 29 | # Configure audit file handler 30 | self.file_handler = logging.FileHandler(self.audit_log_file) 31 | self.file_handler.setFormatter( 32 | logging.Formatter( 33 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s', 34 | datefmt='%Y-%m-%d %H:%M:%S' 35 | ) 36 | ) 37 | 38 | # Create audit logger 39 | self.audit_logger = logging.getLogger('certmate.audit') 40 | self.audit_logger.addHandler(self.file_handler) 41 | self.audit_logger.setLevel(logging.INFO) 42 | 43 | def log_operation( 44 | self, 45 | operation: str, 46 | resource_type: str, 47 | resource_id: str, 48 | status: str, 49 | details: Optional[Dict[str, Any]] = None, 50 | user: Optional[str] = None, 51 | ip_address: Optional[str] = None, 52 | error: Optional[str] = None 53 | ) -> None: 54 | """ 55 | Log a certificate operation. 56 | 57 | Args: 58 | operation: Operation type (create, revoke, renew, download, etc.) 59 | resource_type: Resource type (certificate, csr, crl, etc.) 60 | resource_id: Resource identifier 61 | status: Operation status (success, failure, denied) 62 | details: Additional operation details 63 | user: User who performed operation 64 | ip_address: IP address of requester 65 | error: Error message if operation failed 66 | """ 67 | try: 68 | audit_entry = { 69 | 'timestamp': datetime.utcnow().isoformat(), 70 | 'operation': operation, 71 | 'resource_type': resource_type, 72 | 'resource_id': resource_id, 73 | 'status': status, 74 | 'user': user or 'system', 75 | 'ip_address': ip_address or 'unknown', 76 | 'details': details or {}, 77 | 'error': error 78 | } 79 | 80 | # Log to audit file as JSON for easy parsing 81 | self.audit_logger.info(json.dumps(audit_entry)) 82 | 83 | except Exception as e: 84 | logger.error(f"Failed to write audit log: {e}") 85 | 86 | def log_certificate_created( 87 | self, 88 | identifier: str, 89 | common_name: str, 90 | usage: str, 91 | user: Optional[str] = None, 92 | ip_address: Optional[str] = None 93 | ) -> None: 94 | """Log certificate creation.""" 95 | self.log_operation( 96 | operation='create', 97 | resource_type='certificate', 98 | resource_id=identifier, 99 | status='success', 100 | details={ 101 | 'common_name': common_name, 102 | 'usage': usage 103 | }, 104 | user=user, 105 | ip_address=ip_address 106 | ) 107 | 108 | def log_certificate_revoked( 109 | self, 110 | identifier: str, 111 | reason: str, 112 | user: Optional[str] = None, 113 | ip_address: Optional[str] = None 114 | ) -> None: 115 | """Log certificate revocation.""" 116 | self.log_operation( 117 | operation='revoke', 118 | resource_type='certificate', 119 | resource_id=identifier, 120 | status='success', 121 | details={'reason': reason}, 122 | user=user, 123 | ip_address=ip_address 124 | ) 125 | 126 | def log_certificate_renewed( 127 | self, 128 | identifier: str, 129 | user: Optional[str] = None, 130 | ip_address: Optional[str] = None 131 | ) -> None: 132 | """Log certificate renewal.""" 133 | self.log_operation( 134 | operation='renew', 135 | resource_type='certificate', 136 | resource_id=identifier, 137 | status='success', 138 | user=user, 139 | ip_address=ip_address 140 | ) 141 | 142 | def log_certificate_downloaded( 143 | self, 144 | identifier: str, 145 | file_type: str, 146 | user: Optional[str] = None, 147 | ip_address: Optional[str] = None 148 | ) -> None: 149 | """Log certificate file download.""" 150 | self.log_operation( 151 | operation='download', 152 | resource_type='certificate', 153 | resource_id=identifier, 154 | status='success', 155 | details={'file_type': file_type}, 156 | user=user, 157 | ip_address=ip_address 158 | ) 159 | 160 | def log_batch_operation( 161 | self, 162 | operation: str, 163 | total: int, 164 | successful: int, 165 | failed: int, 166 | user: Optional[str] = None, 167 | ip_address: Optional[str] = None 168 | ) -> None: 169 | """Log batch operation (e.g., CSV import).""" 170 | self.log_operation( 171 | operation=f'batch_{operation}', 172 | resource_type='certificates', 173 | resource_id='batch', 174 | status='success', 175 | details={ 176 | 'total': total, 177 | 'successful': successful, 178 | 'failed': failed 179 | }, 180 | user=user, 181 | ip_address=ip_address 182 | ) 183 | 184 | def log_api_request( 185 | self, 186 | endpoint: str, 187 | method: str, 188 | status_code: int, 189 | user: Optional[str] = None, 190 | ip_address: Optional[str] = None, 191 | response_time_ms: Optional[float] = None 192 | ) -> None: 193 | """Log API request.""" 194 | self.log_operation( 195 | operation='api_request', 196 | resource_type='endpoint', 197 | resource_id=endpoint, 198 | status='success' if status_code < 400 else 'failure', 199 | details={ 200 | 'method': method, 201 | 'status_code': status_code, 202 | 'response_time_ms': response_time_ms 203 | }, 204 | user=user, 205 | ip_address=ip_address 206 | ) 207 | 208 | def log_error( 209 | self, 210 | operation: str, 211 | resource_type: str, 212 | resource_id: str, 213 | error_message: str, 214 | user: Optional[str] = None, 215 | ip_address: Optional[str] = None 216 | ) -> None: 217 | """Log operation error.""" 218 | self.log_operation( 219 | operation=operation, 220 | resource_type=resource_type, 221 | resource_id=resource_id, 222 | status='failure', 223 | user=user, 224 | ip_address=ip_address, 225 | error=error_message 226 | ) 227 | 228 | def get_recent_entries(self, limit: int = 100) -> list: 229 | """ 230 | Get recent audit log entries. 231 | 232 | Args: 233 | limit: Maximum number of entries to return 234 | 235 | Returns: 236 | List of audit entries (parsed JSON) 237 | """ 238 | try: 239 | entries = [] 240 | if not self.audit_log_file.exists(): 241 | return entries 242 | 243 | with open(self.audit_log_file, 'r') as f: 244 | lines = f.readlines() 245 | # Get last 'limit' lines 246 | for line in lines[-limit:]: 247 | try: 248 | # Extract JSON from log line (format: timestamp - logger - level - {json}) 249 | if ' - INFO - ' in line: 250 | json_str = line.split(' - INFO - ', 1)[1].strip() 251 | entries.append(json.loads(json_str)) 252 | except (json.JSONDecodeError, IndexError): 253 | continue 254 | 255 | return entries 256 | 257 | except Exception as e: 258 | logger.error(f"Error reading audit logs: {e}") 259 | return [] 260 | 261 | def get_entries_by_resource(self, resource_id: str) -> list: 262 | """ 263 | Get all audit entries for a specific resource. 264 | 265 | Args: 266 | resource_id: Resource identifier 267 | 268 | Returns: 269 | List of matching audit entries 270 | """ 271 | try: 272 | entries = [] 273 | if not self.audit_log_file.exists(): 274 | return entries 275 | 276 | with open(self.audit_log_file, 'r') as f: 277 | for line in f: 278 | try: 279 | if ' - INFO - ' in line: 280 | json_str = line.split(' - INFO - ', 1)[1].strip() 281 | entry = json.loads(json_str) 282 | if entry.get('resource_id') == resource_id: 283 | entries.append(entry) 284 | except (json.JSONDecodeError, IndexError): 285 | continue 286 | 287 | return entries 288 | 289 | except Exception as e: 290 | logger.error(f"Error reading audit logs: {e}") 291 | return [] 292 | --------------------------------------------------------------------------------