ssl-tls-management — Claude Code skill: certs and HTTPS
A skill for SSL/TLS work: issuing Let's Encrypt certificates, automating renewal, running cert-manager in Kubernetes and checking cipher suites. Useful when you set up HTTPS or fix an expired cert.
npx skills add BagelHole/DevOps-Security-Agent-Skills --skill ssl-tls-management -a claude-codeWhat ssl-tls-management does
The skill covers the whole certificate path, from issuing with certbot to automatic renewal and inspecting ciphers with openssl. It ships commands for nginx, standalone mode and DNS challenges, wildcard certificates included. For Kubernetes there's a cert-manager Helm install with ClusterIssuer and Certificate examples.
Renewal runs on a systemd timer that fires certbot renew twice a day, with deploy hooks reloading nginx and haproxy after each update. A separate openssl section handles RSA and ECDSA keys, CSRs with SAN, and self-signed certs for local development.
You need a domain with DNS control for public certificates, root on the web server, and certbot installed. Internal Kubernetes services need a cluster with Helm. No DNS record means the HTTP challenge fails, so check where the domain points before you start.
What it helps with
- issue a Let's Encrypt certificate for a new domain
- set up auto-renewal so HTTPS does not break
- deploy cert-manager and hand out TLS in Kubernetes
- generate a CSR with SAN for an internal CA
- audit cipher suites and TLS versions before a compliance review
Example prompts for the agent
- Set up HTTPS for example.com with certbot and auto-renewal
- Install cert-manager in my cluster and issue a wildcard cert for *.example.com
- Generate a self-signed certificate for local development
How to install ssl-tls-management
1Skills CLI (Vercel)
npx skills add BagelHole/DevOps-Security-Agent-Skills --skill ssl-tls-management -a claude-code2Manually: put the files in place
~/.claude/skills/ssl-tls-management/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/ssl-tls-management/
1Skills CLI (Vercel)
npx skills add BagelHole/DevOps-Security-Agent-Skills --skill ssl-tls-management -a codex2Manually: put the files in place
.agents/skills/ssl-tls-management/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add BagelHole/DevOps-Security-Agent-Skills --skill ssl-tls-management -a cursor2Manually: put the files in place
.cursor/skills/ssl-tls-management/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install BagelHole/DevOps-Security-Agent-Skills ssl-tls-management2Manually: put the files in place
.github/skills/ssl-tls-management/SKILL.md1Skills CLI (Vercel)
npx skills add BagelHole/DevOps-Security-Agent-Skills --skill ssl-tls-management -a gemini-cli2Platform CLI
gemini skills install https://github.com/BagelHole/DevOps-Security-Agent-Skills --path security/network/ssl-tls-managementManually: put the files in place
.devin/skills/ssl-tls-management/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add BagelHole/DevOps-Security-Agent-Skills --skill ssl-tls-management -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about ssl-tls-management
How do I install ssl-tls-management in Claude?
Run npx skills add BagelHole/DevOps-Security-Agent-Skills --skill ssl-tls-management -a claude-code. The skill lands in ~/.claude/skills/ssl-tls-management/SKILL.md.
What does ssl-tls-management do?
It helps issue and renew Let's Encrypt certificates, configure cert-manager in Kubernetes, and work through cipher settings with openssl.
What do I need before using it?
A domain with DNS control for public certificates, root access on the web server, and certbot installed. For Kubernetes, a cluster with Helm.
Source text
SKILL.md · MIT
--- name: ssl-tls-management description: Manage SSL/TLS certificates with Let's Encrypt and internal PKI. Configure secure HTTPS, certificate renewal, and cipher suites. Use when implementing secure communications. license: MIT metadata: author: devops-skills version: "1.0" --- # SSL/TLS Management Manage certificates and secure communications across web servers, Kubernetes clusters, and internal services. ## When to Use This Skill Use this skill when: - Setting up HTTPS for a new web application - Automating certificate renewal with Let's Encrypt - Deploying cert-manager in Kubernetes - Configuring TLS for internal service-to-service communication - Auditing cipher suites and TLS versions for compliance - Responding to an expiring or compromised certificate ## Prerequisites - Domain name with DNS control for public certificates - Root/sudo access on web servers - `certbot` installed for Let's Encrypt - `openssl` CLI available (installed by default on most Linux distros) - Kubernetes cluster with Helm for cert-manager deployment - Understanding of X.509 certificate chain of trust ## Let's Encrypt with Certbot ### Installation and Certificate Issuance ```bash # Install certbot (Ubuntu/Debian) apt update && apt install -y certbot python3-certbot-nginx # Obtain certificate for nginx (interactive) certbot --nginx -d example.com -d www.example.com # Non-interactive mode for automation certbot certonly --nginx \ -d example.com \ -d www.example.com \ --non-interactive \ --agree-tos \ --email [email protected] # Standalone mode (when no web server is running) certbot certonly --standalone \ -d example.com \ --preferred-challenges http # DNS challenge (for wildcard certs) certbot certonly --manual \ --preferred-challenges dns \ -d "*.example.com" \ -d example.com # Using DNS plugin for automation (Cloudflare example) pip install certbot-dns-cloudflare cat > /etc/letsencrypt/cloudflare.ini << 'EOF' dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN EOF chmod 600 /etc/letsencrypt/cloudflare.ini certbot certonly --dns-cloudflare \ --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \ -d "*.example.com" \ -d example.com ``` ### Renewal Automation ```bash # Test renewal certbot renew --dry-run # Systemd timer (preferred over cron) cat > /etc/systemd/system/certbot-renewal.service << 'EOF' [Unit] Description=Certbot certificate renewal After=network-online.target [Service] Type=oneshot ExecStart=/usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx" EOF cat > /etc/systemd/system/certbot-renewal.timer << 'EOF' [Unit] Description=Run certbot renewal twice daily [Timer] OnCalendar=*-*-* 00,12:00:00 RandomizedDelaySec=3600 Persistent=true [Install] WantedBy=timers.target EOF systemctl enable --now certbot-renewal.timer # Verify timer is active systemctl list-timers certbot-renewal.timer # Renewal hooks for post-renewal actions mkdir -p /etc/letsencrypt/renewal-hooks/deploy cat > /etc/letsencrypt/renewal-hooks/deploy/reload-services.sh << 'HOOK' #!/bin/bash systemctl reload nginx # Also reload other services using the cert systemctl reload haproxy 2>/dev/null || true HOOK chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-services.sh ``` ## cert-manager for Kubernetes ### Installation ```bash # Install with Helm helm repo add jetstack https://charts.jetstack.io helm repo update helm install cert-manager jetstack/cert-manager \ --namespace cert-manager \ --create-namespace \ --version v1.14.0 \ --set installCRDs=true \ --set prometheus.enabled=true # Verify installation kubectl get pods -n cert-manager kubectl get crds | grep cert-manager ``` ### ClusterIssuer Configurations ```yaml # letsencrypt-staging (use for testing first) apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-staging spec: acme: server: https://acme-staging-v02.api.letsencrypt.org/directory email: [email protected] privateKeySecretRef: name: letsencrypt-staging solvers: - http01: ingress: class: nginx --- # letsencrypt-prod apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-prod spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: [email protected] privateKeySecretRef: name: letsencrypt-prod solvers: - http01: ingress: class: nginx --- # DNS challenge solver (for wildcard certs with Cloudflare) apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-prod-dns spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: [email protected] privateKeySecretRef: name: letsencrypt-prod-dns solvers: - dns01: cloudflare: apiTokenSecretRef: name: cloudflare-api-token key: api-token --- # Self-signed CA issuer for internal services apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: selfsigned-ca spec: selfSigned: {} ``` ### Certificate Resources ```yaml # Public-facing certificate apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: example-cert namespace: default spec: secretName: example-tls issuerRef: name: letsencrypt-prod kind: ClusterIssuer dnsNames: - example.com - www.example.com duration: 2160h # 90 days renewBefore: 720h # 30 days before expiry --- # Wildcard certificate apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: wildcard-cert namespace: default spec: secretName: wildcard-tls issuerRef: name: letsencrypt-prod-dns kind: ClusterIssuer dnsNames: - "*.example.com" - example.com --- # Ingress with automatic TLS apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: example-ingress annotations: cert-manager.io/cluster-issuer: letsencrypt-prod spec: tls: - hosts: - example.com secretName: example-tls rules: - host: example.com http: paths: - path: / pathType: Prefix backend: service: name: web port: number: 80 ``` ## OpenSSL Commands Reference ```bash # Generate a private key openssl genrsa -out server.key 4096 # Generate an ECDSA key (preferred for performance) openssl ecparam -genkey -name prime256v1 -out server-ec.key # Generate a CSR (Certificate Signing Request) openssl req -new -key server.key -out server.csr \ -subj "/C=US/ST=California/L=San Francisco/O=Acme Corp/CN=example.com" # Generate CSR with SAN (Subject Alternative Names) openssl req -new -key server.key -out server.csr -config <(cat <<EOF [req] default_bits = 4096 distinguished_name = dn req_extensions = san prompt = no [dn] CN = example.com O = Acme Corp C = US [san] subjectAltName = DNS:example.com,DNS:www.example.com,DNS:api.example.com EOF ) # Generate self-signed certificate (development/testing) openssl req -x509 -nodes -days 365 -newkey rsa:4096 \ -keyout selfsigned.key -out selfsigned.crt \ -subj "/CN=localhost" # View certificate details openssl x509 -in cert.pem -noout -text # Check certificate expiration date openssl x509 -in cert.pem -noout -dates # Check remote certificate openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | \ openssl x509 -noout -dates -subject -issuer # Verify certificate chain openssl verify -CAfile ca-bundle.crt server.crt # Check certificate chain from remote server openssl s_client -connect example.com:443 -showcerts 2>/dev/null | \ openssl x509 -noout -text # Convert PEM to PKCS12 openssl pkcs12 -export -out cert.pfx -inkey server.key -in server.crt -certfile ca.crt # Convert PKCS12 to PEM openssl pkcs12 -in cert.pfx -out cert.pem -nodes # Test TLS connection and cipher negotiation openssl s_client -connect example.com:443 -tls1_3 openssl s_client -connect example.com:443 -cipher 'ECDHE-RSA-AES256-GCM-SHA384' ``` ## Strong TLS Configuration ### Nginx ```nginx server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # Protocol versions ssl_protocols TLSv1.2 TLSv1.3; # Cipher suites (TLS 1.2) ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; # Session settings ssl_session_timeout 1d; ssl_session_cache shared:SSL:10m; ssl_session_tickets off; # OCSP stapling ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem; resolver 8.8.8.8 8.8.4.4 valid=300s; resolver_timeout 5s; # Security headers add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options DENY always; # HTTP to HTTPS redirect (in separate server block) } server { listen 80; server_name example.com www.example.com; return 301 https://$host$request_uri; } ``` ### Apache ```apache <VirtualHost *:443> ServerName example.com SSLEngine on SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1 SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384 SSLHonorCipherOrder off SSLUseStapling on SSLStaplingCache shmcb:/tmp/stapling_cache(128000) Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" </VirtualHost> ``` ## Certificate Monitoring ```bash #!/bin/bash # cert-monitor.sh - Monitor certificate expiration across hosts WARN_DAYS=30 CRIT_DAYS=7 HOSTS=( "example.com:443" "api.example.com:443" "admin.example.com:443" ) for host in "${HOSTS[@]}"; do expiry=$(echo | openssl s_client -connect "$host" -servername "${host%%:*}" 2>/dev/null | \ openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2) if [ -z "$expiry" ]; then echo "ERROR: Cannot connect to $host" continue fi expiry_epoch=$(date -d "$expiry" +%s) now_epoch=$(date +%s) days_left=$(( (expiry_epoch - now_epoch) / 86400 )) if [ "$days_left" -le "$CRIT_DAYS" ]; then echo "CRITICAL: $host expires in $days_left days ($expiry)" elif [ "$days_left" -le "$WARN_DAYS" ]; then echo "WARNING: $host expires in $days_left days ($expiry)" else echo "OK: $host expires in $days_left days ($expiry)" fi done ``` ### Prometheus cert-manager Metrics ```yaml # Alert on expiring certificates in Kubernetes groups: - name: cert-manager rules: - alert: CertificateExpiringSoon expr: certmanager_certificate_expiration_timestamp_seconds - time() < 7 * 24 * 3600 for: 1h labels: severity: critical annotations: summary: "Certificate {{ $labels.name }} expires in less than 7 days" - alert: CertificateNotReady expr: certmanager_certificate_ready_status{condition="True"} == 0 for: 15m labels: severity: warning annotations: summary: "Certificate {{ $labels.name }} is not ready" ``` ## Troubleshooting | Problem | Cause | Solution | |---------|-------|----------| | Certbot fails with "connection refused" | Port 80 blocked by firewall | Open port 80 for ACME HTTP-01 challenge | | "Too many certificates already issued" | Let's Encrypt rate limit hit | Use staging endpoint for testing; wait for rate limit reset | | cert-manager challenge stuck pending | Ingress or DNS misconfigured | Check `kubectl describe challenge`; verify DNS records | | Mixed content warnings | HTTP resources on HTTPS page | Update all asset URLs to HTTPS; use CSP headers | | OCSP stapling not working | Resolver not configured | Add `resolver` directive in nginx; verify outbound DNS | | Intermediate cert missing | Incomplete chain served | Use `fullchain.pem` not `cert.pem`; verify with `openssl s_client -showcerts` | | TLS handshake failure | Client doesn't support offered ciphers | Add TLS 1.2 support; check cipher suite compatibility | ## Best Practices - Automate renewal with systemd timers or cert-manager - Monitor expiration dates with alerting (30-day and 7-day warnings) - Use only TLS 1.2 and TLS 1.3 - Enable HSTS with long max-age and includeSubDomains - Enable OCSP stapling to improve handshake performance - Use ECDSA keys for better performance where possible - Test configuration with SSL Labs (ssllabs.com/ssltest) - Keep private keys secure with proper file permissions (0600) - Rotate certificates before expiry, not after - Maintain a certificate inventory across all services ## Related Skills - [hashicorp-vault](../../secrets/hashicorp-vault/) - PKI management - [waf-setup](../waf-setup/) - Web protection - [zero-trust](../zero-trust/) - mTLS and identity-based access
© devops-skills · MIT · BagelHole/DevOps-Security-Agent-Skills