# HTTPS/TLS Setup Guide - Svrnty.MCP.Server **Purpose**: Production-grade HTTPS/TLS configuration for secure MCP server deployment **Audience**: DevOps engineers, system administrators **Last Updated**: 2025-10-19 **Version**: 1.0.0 --- ## Table of Contents 1. [Overview](#overview) 2. [Prerequisites](#prerequisites) 3. [Development Setup](#development-setup) 4. [Production Setup](#production-setup) 5. [Certificate Management](#certificate-management) 6. [Security Headers (HSTS)](#security-headers-hsts) 7. [Testing HTTPS](#testing-https) 8. [Troubleshooting](#troubleshooting) --- ## Overview Svrnty.MCP.Server supports HTTPS/TLS through ASP.NET Core's Kestrel web server. This guide covers configuration for both development and production environments. **Security Benefits:** - Encrypted communication (prevents eavesdropping) - Client/server authentication - Data integrity verification - Compliance with security best practices **Default Behavior:** - HTTP only (for development ease) - HTTPS must be explicitly configured - Uses Kestrel's built-in TLS support --- ## Prerequisites ### Development - .NET 8.0 SDK - OpenSSL or dotnet dev-certs tool - PowerShell (Windows) or bash (Linux/macOS) ### Production - Valid TLS certificate (from CA or Let's Encrypt) - Private key file - Certificate chain (intermediate + root CA certs) - Reverse proxy (optional): Nginx, Traefik, or Kestrel standalone --- ## Development Setup ### Option 1: ASP.NET Core Development Certificate (Recommended) **Step 1: Generate Development Certificate** **Windows / macOS / Linux:** ```bash # Generate and trust dev certificate dotnet dev-certs https --trust # Verify installation dotnet dev-certs https --check ``` **Step 2: Configure appsettings.Development.json** Create or update `appsettings.Development.json` in the Server project: ```json { "Kestrel": { "Endpoints": { "Http": { "Url": "http://localhost:5050" }, "Https": { "Url": "https://localhost:5051", "Certificate": { "Subject": "CN=localhost", "Store": "My", "Location": "CurrentUser", "AllowInvalid": true } } } }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } } } ``` **Step 3: Run Server with HTTPS** ```bash cd Svrnty.MCP.Server/samples/CodexMcpServer dotnet run --environment Development # Server will listen on: # - HTTP: http://localhost:5050 # - HTTPS: https://localhost:5051 ``` **Step 4: Test HTTPS Endpoint** ```bash # Test health endpoint curl -k https://localhost:5051/health # Test MCP invoke endpoint curl -k -X POST https://localhost:5051/mcp/invoke \ -H "Content-Type: application/json" \ -H "X-API-Key: your-api-key" \ -d '{ "jsonrpc": "2.0", "method": "tools/list", "id": "1" }' ``` > **Note**: `-k` flag skips certificate validation (dev only!) ### Option 2: Custom Development Certificate with OpenSSL **Step 1: Generate Self-Signed Certificate** ```bash # Create directory for certs mkdir -p /home/svrnty/codex/Svrnty.MCP.Server/certs # Generate private key openssl genrsa -out certs/localhost.key 2048 # Generate certificate signing request (CSR) openssl req -new -key certs/localhost.key -out certs/localhost.csr \ -subj "/C=CA/ST=Quebec/L=Montreal/O=Svrnty/CN=localhost" # Generate self-signed certificate (valid for 365 days) openssl x509 -req -days 365 -in certs/localhost.csr \ -signkey certs/localhost.key -out certs/localhost.crt # Convert to PFX format (required by Kestrel) openssl pkcs12 -export -out certs/localhost.pfx \ -inkey certs/localhost.key -in certs/localhost.crt \ -passout pass:YourSecurePassword ``` **Step 2: Configure appsettings.Development.json** ```json { "Kestrel": { "Endpoints": { "Https": { "Url": "https://localhost:5051", "Certificate": { "Path": "certs/localhost.pfx", "Password": "YourSecurePassword" } } } } } ``` **Security**: Add `certs/` directory to `.gitignore` to avoid committing private keys. --- ## Production Setup ### Option 1: Certificate from File (Recommended for Kubernetes/Docker) **Step 1: Obtain Production Certificate** Use one of: - **Let's Encrypt** (free, automated renewal) - **Commercial CA** (DigiCert, Sectigo, etc.) - **Internal PKI** (corporate certificate authority) **Step 2: Prepare Certificate Files** Ensure you have: - `server.crt` - Server certificate - `server.key` - Private key - `ca-bundle.crt` - Intermediate + root CA certificates (optional) **Step 3: Convert to PFX Format** ```bash # Combine cert + key + chain into PFX openssl pkcs12 -export -out server.pfx \ -inkey server.key \ -in server.crt \ -certfile ca-bundle.crt \ -passout pass:ProductionSecurePassword ``` **Step 4: Deploy Certificate** **Docker/Kubernetes:** ```bash # Create Kubernetes secret kubectl create secret generic mcp-server-tls \ --from-file=server.pfx=server.pfx \ --from-literal=password=ProductionSecurePassword # Mount in deployment apiVersion: apps/v1 kind: Deployment metadata: name: mcp-server spec: template: spec: containers: - name: mcp-server volumeMounts: - name: tls-cert mountPath: /app/certs readOnly: true env: - name: CERTIFICATE_PASSWORD valueFrom: secretKeyRef: name: mcp-server-tls key: password volumes: - name: tls-cert secret: secretName: mcp-server-tls ``` **Step 5: Configure appsettings.Production.json** ```json { "Kestrel": { "Endpoints": { "Https": { "Url": "https://*:5051", "Certificate": { "Path": "/app/certs/server.pfx", "Password": "" // Read from environment variable }, "Protocols": "Http1AndHttp2" } } }, "AllowedHosts": "*" } ``` **Step 6: Set Environment Variable for Password** ```bash export KESTREL__CERTIFICATES__DEFAULT__PASSWORD="ProductionSecurePassword" ``` Or in `docker-compose.yml`: ```yaml services: mcp-server: environment: - KESTREL__CERTIFICATES__DEFAULT__PASSWORD=${CERTIFICATE_PASSWORD} volumes: - ./certs/server.pfx:/app/certs/server.pfx:ro ``` ### Option 2: Certificate from Azure Key Vault **Step 1: Store Certificate in Key Vault** ```bash az keyvault certificate import \ --vault-name your-keyvault \ --name mcp-server-cert \ --file server.pfx \ --password ProductionSecurePassword ``` **Step 2: Configure appsettings.Production.json** ```json { "Kestrel": { "Endpoints": { "Https": { "Url": "https://*:5051", "Certificate": { "KeyVaultUri": "https://your-keyvault.vault.azure.net/", "CertificateName": "mcp-server-cert" } } } } } ``` **Step 3: Grant Managed Identity Access** ```bash az keyvault set-policy \ --name your-keyvault \ --object-id \ --certificate-permissions get ``` ### Option 3: Reverse Proxy (Nginx/Traefik) with TLS Termination **Best for**: Load balancing, multiple servers, centralized certificate management **Architecture**: ``` Client → HTTPS → Nginx/Traefik (TLS termination) → HTTP → MCP Server (5050) ``` **Nginx Configuration** (`/etc/nginx/sites-available/mcp-server`): ```nginx server { listen 443 ssl http2; server_name mcp.example.com; ssl_certificate /etc/nginx/ssl/server.crt; ssl_certificate_key /etc/nginx/ssl/server.key; # Modern TLS configuration ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256'; ssl_prefer_server_ciphers off; # HSTS add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; location / { proxy_pass http://localhost:5050; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } # Redirect HTTP to HTTPS server { listen 80; server_name mcp.example.com; return 301 https://$server_name$request_uri; } ``` **Traefik Configuration** (`docker-compose.yml`): ```yaml services: traefik: image: traefik:v2.10 command: - "--api.insecure=true" - "--providers.docker=true" - "--entrypoints.web.address=:80" - "--entrypoints.websecure.address=:443" - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true" - "--certificatesresolvers.letsencrypt.acme.email=admin@example.com" - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" ports: - "80:80" - "443:443" volumes: - "/var/run/docker.sock:/var/run/docker.sock:ro" - "./letsencrypt:/letsencrypt" mcp-server: image: openharbor/mcp-server:latest labels: - "traefik.enable=true" - "traefik.http.routers.mcp.rule=Host(`mcp.example.com`)" - "traefik.http.routers.mcp.entrypoints=websecure" - "traefik.http.routers.mcp.tls.certresolver=letsencrypt" - "traefik.http.services.mcp.loadbalancer.server.port=5050" ``` --- ## Certificate Management ### Automatic Renewal with Let's Encrypt **Using Certbot:** ```bash # Install certbot sudo apt-get install certbot # Obtain certificate sudo certbot certonly --standalone -d mcp.example.com # Certificates location: /etc/letsencrypt/live/mcp.example.com/ # - fullchain.pem (certificate + chain) # - privkey.pem (private key) # Convert to PFX for Kestrel sudo openssl pkcs12 -export \ -out /etc/letsencrypt/live/mcp.example.com/server.pfx \ -inkey /etc/letsencrypt/live/mcp.example.com/privkey.pem \ -in /etc/letsencrypt/live/mcp.example.com/fullchain.pem \ -passout pass:YourSecurePassword # Auto-renewal (certbot creates cron job automatically) sudo certbot renew --dry-run ``` ### Certificate Rotation **Step 1: Update Certificate File** Replace `server.pfx` with new certificate. **Step 2: Reload Without Downtime** ```bash # Docker docker exec mcp-server kill -HUP 1 # Kubernetes kubectl rollout restart deployment/mcp-server # Systemd sudo systemctl reload mcp-server ``` **Step 3: Verify New Certificate** ```bash echo | openssl s_client -connect mcp.example.com:443 -servername mcp.example.com 2>/dev/null | openssl x509 -noout -dates ``` --- ## Security Headers (HSTS) **HTTP Strict Transport Security (HSTS)** forces browsers to use HTTPS. **Method 1: Kestrel Middleware** (Recommended) Update `Program.cs` in `CodexMcpServer`: ```csharp var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); // Add HSTS middleware (production only) if (!app.Environment.IsDevelopment()) { app.UseHsts(); } // Redirect HTTP to HTTPS app.UseHttpsRedirection(); // ... rest of configuration app.Run(); ``` **Method 2: Custom Middleware** ```csharp app.Use(async (context, next) => { if (!context.Request.IsHttps) { var httpsUrl = $"https://{context.Request.Host}{context.Request.Path}{context.Request.QueryString}"; context.Response.Redirect(httpsUrl, permanent: true); return; } // Add HSTS header context.Response.Headers.Add("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); await next(); }); ``` **Method 3: Reverse Proxy (Nginx)** Already shown in [Option 3](#option-3-reverse-proxy-nginxtraefik-with-tls-termination) above. --- ## Testing HTTPS ### 1. Health Check ```bash curl https://mcp.example.com/health ``` Expected output: ```json { "status": "Healthy", "service": "MCP Server", "timestamp": "2025-10-19T12:00:00Z" } ``` ### 2. MCP Invoke Endpoint ```bash curl -X POST https://mcp.example.com/mcp/invoke \ -H "Content-Type: application/json" \ -H "X-API-Key: your-api-key" \ -d '{ "jsonrpc": "2.0", "method": "tools/list", "id": "test-1" }' ``` ### 3. TLS Configuration Check ```bash # Check TLS version and cipher suite nmap --script ssl-enum-ciphers -p 443 mcp.example.com # Check certificate validity echo | openssl s_client -connect mcp.example.com:443 -servername mcp.example.com 2>/dev/null | openssl x509 -noout -text ``` ### 4. HSTS Validation ```bash curl -I https://mcp.example.com/health | grep -i strict-transport-security ``` Expected output: ``` Strict-Transport-Security: max-age=31536000; includeSubDomains ``` ### 5. SSL Labs Test For public-facing servers, use [SSL Labs](https://www.ssllabs.com/ssltest/) to get an A+ rating. --- ## Troubleshooting ### Issue: "Unable to configure HTTPS endpoint" **Symptom:** ``` Unhandled exception. System.InvalidOperationException: Unable to configure HTTPS endpoint. ``` **Solution:** 1. Verify certificate file exists at specified path 2. Check file permissions (readable by server process) 3. Verify password is correct 4. Check certificate format (must be PFX for Kestrel) ```bash # Verify PFX file openssl pkcs12 -info -in server.pfx -noout ``` ### Issue: "The certificate chain was issued by an authority that is not trusted" **Symptom:** Clients reject the certificate as untrusted. **Solution:** 1. Ensure CA bundle is included in PFX 2. Add intermediate certificates to certificate store 3. For development, use `dotnet dev-certs https --trust` ### Issue: "Connection reset" or "SSL handshake failed" **Symptom:** Client cannot establish TLS connection. **Solution:** 1. Check firewall allows port 443 2. Verify TLS protocols match (client vs. server) 3. Check cipher suite compatibility ```bash # Test with specific TLS version openssl s_client -connect mcp.example.com:443 -tls1_2 ``` ### Issue: Certificate Expired **Symptom:** ``` The remote certificate is invalid according to the validation procedure. ``` **Solution:** 1. Check certificate expiry: ```bash echo | openssl s_client -connect mcp.example.com:443 2>/dev/null | openssl x509 -noout -enddate ``` 2. Renew certificate (see [Certificate Management](#certificate-management)) 3. Deploy new certificate ### Issue: HSTS Not Working **Symptom:** Browser still allows HTTP connections. **Solution:** 1. Verify HSTS header is sent: ```bash curl -I https://mcp.example.com/health | grep Strict-Transport-Security ``` 2. Clear browser HSTS cache (Chrome: `chrome://net-internals/#hsts`) 3. Ensure middleware is configured correctly --- ## Production Checklist Before deploying to production, verify: - [ ] Valid TLS certificate from trusted CA - [ ] Certificate includes full chain (intermediate + root) - [ ] Private key is stored securely (Kubernetes secret, Azure Key Vault, etc.) - [ ] Certificate password is stored in environment variable (not in config) - [ ] Certificate expiry monitoring is configured - [ ] Automatic renewal is set up (Let's Encrypt, certbot) - [ ] HSTS header is enabled - [ ] HTTP → HTTPS redirect is configured - [ ] TLS 1.2+ is enforced (no SSLv3, TLS 1.0, TLS 1.1) - [ ] Strong cipher suites are configured - [ ] Certificate Common Name (CN) matches domain - [ ] Firewall allows port 443 - [ ] Health check endpoint is accessible via HTTPS - [ ] Load balancer (if used) is configured for TLS passthrough or termination - [ ] Monitoring/alerting is set up for certificate expiry --- ## References **ASP.NET Core Documentation:** - [Configure HTTPS in Kestrel](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/endpoints) - [Enforce HTTPS](https://learn.microsoft.com/en-us/aspnet/core/security/enforcing-ssl) - [HSTS Middleware](https://learn.microsoft.com/en-us/aspnet/core/security/enforcing-ssl#http-strict-transport-security-protocol-hsts) **Security Best Practices:** - [Mozilla SSL Configuration Generator](https://ssl-config.mozilla.org/) - [OWASP TLS Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Protection_Cheat_Sheet.html) - [SSL Labs Best Practices](https://github.com/ssllabs/research/wiki/SSL-and-TLS-Deployment-Best-Practices) **Certificate Authorities:** - [Let's Encrypt](https://letsencrypt.org/) - Free automated certificates - [DigiCert](https://www.digicert.com/) - Commercial CA - [Sectigo](https://sectigo.com/) - Commercial CA --- **Document Version**: 1.0.0 **Last Updated**: 2025-10-19 **Maintained By**: Svrnty Development Team **Related**: [Deployment Guide](deployment-guide.md), [Security Best Practices](../security/security-guide.md)