Skip to content

Latest commit

 

History

History
553 lines (445 loc) · 14.1 KB

File metadata and controls

553 lines (445 loc) · 14.1 KB

TLS/HTTPS Configuration

GitLab MCP Server supports two approaches for secure HTTPS connections:

  1. Direct TLS Termination - Server handles TLS with certificate files
  2. Reverse Proxy - External proxy (nginx, Envoy, Caddy, Traefik) handles TLS

Quick Reference

Approach Best For HTTP/2 Auto-Renewal
Direct TLS Development, simple deployments No Manual
Reverse Proxy Production, enterprise Yes Yes (Let's Encrypt)

Option 1: Direct TLS Termination

The server can directly handle TLS/HTTPS using certificate files. This is suitable for simple deployments or development environments.

Environment Variables

Variable Description Required
SSL_CERT_PATH Path to PEM certificate file Yes
SSL_KEY_PATH Path to PEM private key file Yes
SSL_CA_PATH Path to CA certificate chain (for client cert validation) No
SSL_PASSPHRASE Passphrase for encrypted private keys No

Example - Self-Signed Certificate (Development)

# Generate self-signed certificate (for testing only)
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout server.key -out server.crt \
  -subj "/CN=gitlab-mcp.local"

Example - Direct HTTPS with Docker

docker run -d \
  -e PORT=3000 \
  -e SSL_CERT_PATH=/certs/server.crt \
  -e SSL_KEY_PATH=/certs/server.key \
  -e GITLAB_TOKEN=your_token \
  -e GITLAB_API_URL=https://gitlab.com \
  -v $(pwd)/certs:/certs:ro \
  -p 3000:3000 \
  ghcr.io/structured-world/gitlab-mcp:latest

MCP Client Configuration (HTTPS)

{
  "mcpServers": {
    "gitlab": {
      "type": "streamable-http",
      "url": "https://your-server.com:3000/mcp"
    }
  }
}

Option 2: Reverse Proxy (Recommended for Production)

For production deployments, use a reverse proxy to handle TLS termination. This provides:

  • HTTP/2 support with proper ALPN negotiation
  • Automatic certificate renewal (Let's Encrypt via Certbot, Caddy, etc.)
  • Load balancing capabilities
  • Better security (proxy filters traffic before reaching application)
  • Centralized TLS management for multiple services

Trust Proxy Configuration

When behind a reverse proxy, configure TRUST_PROXY to properly handle X-Forwarded-* headers:

Variable Description
TRUST_PROXY Enable Express trust proxy for X-Forwarded-* headers

Trust Proxy Values:

Value Description
true or 1 Trust all proxies (use only if you control all proxies)
false or 0 Disable trust proxy
loopback Trust loopback addresses (127.0.0.1, ::1)
linklocal Trust link-local addresses (169.254.0.0/16, fe80::/10)
uniquelocal Trust unique-local addresses (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7)
Number (e.g., 1) Trust the nth hop from the front-facing proxy
IP addresses Trust specific proxy IPs (comma-separated)

Nginx Configuration

Full nginx configuration with HTTP/2 and SSE support.

nginx.conf

upstream gitlab_mcp {
    server 127.0.0.1:3002;
    keepalive 32;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name gitlab-mcp.example.com;

    # TLS configuration
    ssl_certificate /etc/letsencrypt/live/gitlab-mcp.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/gitlab-mcp.example.com/privkey.pem;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;

    # Modern TLS configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    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;

    # HSTS (optional, recommended)
    add_header Strict-Transport-Security "max-age=63072000" always;

    # Proxy settings for MCP
    location / {
        proxy_pass http://gitlab_mcp;
        proxy_http_version 1.1;
        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;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Port $server_port;

        # SSE support (critical for MCP)
        proxy_set_header Connection '';
        proxy_buffering off;
        proxy_cache off;
        proxy_read_timeout 86400s;
        chunked_transfer_encoding off;
    }
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name gitlab-mcp.example.com;
    return 301 https://$server_name$request_uri;
}

Docker Compose with Nginx

version: '3.8'

services:
  gitlab-mcp:
    image: ghcr.io/structured-world/gitlab-mcp:latest
    environment:
      - PORT=3002
      - HOST=0.0.0.0
      - TRUST_PROXY=true
      - GITLAB_TOKEN=${GITLAB_TOKEN}
      - GITLAB_API_URL=https://gitlab.com
    expose:
      - "3002"
    networks:
      - internal

  nginx:
    image: nginx:alpine
    ports:
      - "443:443"
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/letsencrypt:ro
    depends_on:
      - gitlab-mcp
    networks:
      - internal

networks:
  internal:

Envoy Configuration

Envoy proxy with HTTP/2 and TLS support.

envoy.yaml

static_resources:
  listeners:
    - name: listener_https
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 443
      filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: ingress_http
                codec_type: AUTO
                http2_protocol_options:
                  max_concurrent_streams: 100
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: gitlab_mcp
                      domains: ["*"]
                      routes:
                        - match:
                            prefix: "/"
                          route:
                            cluster: gitlab_mcp_cluster
                            timeout: 0s  # Disable timeout for SSE
                http_filters:
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
          transport_socket:
            name: envoy.transport_sockets.tls
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
              common_tls_context:
                tls_certificates:
                  - certificate_chain:
                      filename: /etc/envoy/certs/server.crt
                    private_key:
                      filename: /etc/envoy/certs/server.key
                alpn_protocols: ["h2", "http/1.1"]

  clusters:
    - name: gitlab_mcp_cluster
      connect_timeout: 30s
      type: STRICT_DNS
      lb_policy: ROUND_ROBIN
      load_assignment:
        cluster_name: gitlab_mcp_cluster
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: gitlab-mcp
                      port_value: 3002

Docker Compose with Envoy

version: '3.8'

services:
  gitlab-mcp:
    image: ghcr.io/structured-world/gitlab-mcp:latest
    environment:
      - PORT=3002
      - HOST=0.0.0.0
      - TRUST_PROXY=true
      - GITLAB_TOKEN=${GITLAB_TOKEN}
      - GITLAB_API_URL=https://gitlab.com
    expose:
      - "3002"
    networks:
      - internal

  envoy:
    image: envoyproxy/envoy:v1.28-latest
    ports:
      - "443:443"
    volumes:
      - ./envoy.yaml:/etc/envoy/envoy.yaml:ro
      - ./certs:/etc/envoy/certs:ro
    depends_on:
      - gitlab-mcp
    networks:
      - internal

networks:
  internal:

Caddy Configuration

Caddy automatically obtains and renews TLS certificates via Let's Encrypt.

Caddyfile

gitlab-mcp.example.com {
    reverse_proxy gitlab-mcp:3002 {
        flush_interval -1  # Required for SSE
    }
}

Docker Compose with Caddy

version: '3.8'

services:
  gitlab-mcp:
    image: ghcr.io/structured-world/gitlab-mcp:latest
    environment:
      - PORT=3002
      - HOST=0.0.0.0
      - TRUST_PROXY=true
      - GITLAB_TOKEN=${GITLAB_TOKEN}
      - GITLAB_API_URL=https://gitlab.com
    expose:
      - "3002"
    networks:
      - internal

  caddy:
    image: caddy:alpine
    ports:
      - "443:443"
      - "80:80"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
    depends_on:
      - gitlab-mcp
    networks:
      - internal

volumes:
  caddy_data:

networks:
  internal:

Traefik Configuration

Traefik with automatic Let's Encrypt certificates.

traefik.yml (Static Configuration)

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"
    http2:
      maxConcurrentStreams: 250

certificatesResolvers:
  letsencrypt:
    acme:
      email: admin@example.com
      storage: /letsencrypt/acme.json
      httpChallenge:
        entryPoint: web

Dynamic Configuration (File or Labels)

http:
  routers:
    gitlab-mcp:
      rule: "Host(`gitlab-mcp.example.com`)"
      entryPoints:
        - websecure
      service: gitlab-mcp
      tls:
        certResolver: letsencrypt

  services:
    gitlab-mcp:
      loadBalancer:
        servers:
          - url: "http://gitlab-mcp:3002"

Docker Compose with Traefik

version: '3.8'

services:
  gitlab-mcp:
    image: ghcr.io/structured-world/gitlab-mcp:latest
    environment:
      - PORT=3002
      - HOST=0.0.0.0
      - TRUST_PROXY=true
      - GITLAB_TOKEN=${GITLAB_TOKEN}
      - GITLAB_API_URL=https://gitlab.com
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.gitlab-mcp.rule=Host(`gitlab-mcp.example.com`)"
      - "traefik.http.routers.gitlab-mcp.entrypoints=websecure"
      - "traefik.http.routers.gitlab-mcp.tls.certresolver=letsencrypt"
      - "traefik.http.services.gitlab-mcp.loadbalancer.server.port=3002"
    networks:
      - internal

  traefik:
    image: traefik:v2.10
    ports:
      - "443:443"
      - "80:80"
    volumes:
      - ./traefik.yml:/etc/traefik/traefik.yml:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - letsencrypt:/letsencrypt
    networks:
      - internal

volumes:
  letsencrypt:

networks:
  internal:

HTTP/2 Support

HTTP/2 is best supported via reverse proxy for several reasons:

Feature Direct TLS Reverse Proxy
HTTP/2 Limited Full support
ALPN Negotiation Manual Automatic
Connection Multiplexing Basic Optimized
HTTP/1.1 Fallback Manual Automatic
SSE Compatibility Works Works

Why Reverse Proxy for HTTP/2?

  1. ALPN Negotiation - Reverse proxies handle HTTP/2 protocol negotiation properly
  2. Connection Multiplexing - Proxies optimize HTTP/2 stream management
  3. Fallback Support - Automatic fallback to HTTP/1.1 for incompatible clients
  4. SSE Compatibility - Server-Sent Events work over HTTP/2 when properly configured

Note: Direct HTTP/2 support from Node.js Express requires additional setup and may have compatibility issues with some MCP clients. Using a reverse proxy is the recommended approach.


Security Best Practices

TLS Configuration

  1. Use TLS 1.2+ only - Disable TLS 1.0 and 1.1
  2. Modern cipher suites - Prefer ECDHE with AES-GCM
  3. Enable HSTS - Strict-Transport-Security header
  4. Certificate chain - Include full chain in certificate file

Network Security

  1. Bind to localhost when using reverse proxy - HOST=127.0.0.1
  2. Configure TRUST_PROXY correctly - Only trust proxies you control
  3. Use firewall rules - Restrict direct access to backend port
  4. Separate networks - Use Docker networks to isolate services

Certificate Management

  1. Use Let's Encrypt for automatic renewal (Caddy, Certbot)
  2. Monitor expiration - Set up alerts for certificate expiry
  3. Rotate certificates - Don't use certificates beyond their validity
  4. Secure private keys - Restrict file permissions (chmod 600)

Production Checklist

  • TLS 1.2+ only, modern cipher suites
  • HSTS header enabled
  • Certificate auto-renewal configured
  • Backend bound to localhost or internal network
  • TRUST_PROXY set appropriately
  • Firewall rules in place
  • Monitoring and alerting configured

Troubleshooting

Certificate Issues

"Certificate not trusted"

  • Ensure full certificate chain is included
  • Check certificate matches domain name
  • Verify certificate is not expired

"Unable to read certificate"

  • Check file permissions (readable by server process)
  • Verify paths are correct and absolute
  • Ensure certificate is in PEM format

Connection Issues

"Connection refused"

  • Check server is running and listening on correct port
  • Verify firewall allows traffic
  • Check HOST binding (0.0.0.0 vs 127.0.0.1)

"SSE connection drops"

  • Disable proxy buffering (proxy_buffering off)
  • Set long read timeout (proxy_read_timeout 86400s)
  • Clear connection header (proxy_set_header Connection '')

Trust Proxy Issues

"req.ip shows proxy IP instead of client IP"

  • Ensure TRUST_PROXY is set correctly
  • Verify proxy sends X-Forwarded-For header
  • Check proxy hop count if using numeric value

Environment Variables Reference

Variable Description Default
SSL_CERT_PATH PEM certificate file path -
SSL_KEY_PATH PEM private key file path -
SSL_CA_PATH CA certificate chain path -
SSL_PASSPHRASE Private key passphrase -
TRUST_PROXY Trust proxy setting -
HOST Server bind address 127.0.0.1
PORT Server listen port 3002