Swithing from nginx - vmware reverse proxy

Hello I’m switching from nginx to caddy

1. The problem I’m having:

I’m almost done switching from nginx to caddy and it works great. The only thing i don’t manage to get working is my vmware vcenter that i ran behind nginx to get valid certificates.

The two main problems are:

  • Header modification to hide from vcenter that there is a proxy in front of it
  • Whitelist to allow only specific IPs and subnets (I haven’t tried to fix that yet but that is next in line after the first problem).

In the configuration files i have replaced the domain as the adress is not ment for the public.

2. Error messages and/or full log output:

When i visit the adress for the vcenter i get a redirected to the internal adress.
I haven’t managed to transfer all header modifications from nginx to caddy and that is what i need help with. It isn’t visible with curl in a only web browser.

3. Caddy version:

v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk=

4. How I installed and ran Caddy:

Caddy runs in a docker container on the same server as nginx that i’m replacing (and yes. nginx is not running and is disabled)

a. System environment:

Docker compose on ubuntu 24.04

b. Command:

PASTE OVER THIS, BETWEEN THE ``` LINES.
Please use the preview pane to ensure it looks nice.

c. Service/unit/compose file:

services:
  caddy:
    image: serfriz/caddy-crowdsec:latest  # replace with the desired Caddy build name
    container_name: caddy  # feel free to choose your own container name
    restart: "unless-stopped"  # run container unless stopped by user (optional)
    ports:
      - "80:80"  # HTTP port
      - "443:443"  # HTTPS port
      - "443:443/udp"  # HTTP/3 port (optional)
      - "8443:8443" #unifi
    volumes:
      - caddy-data:/data  # volume mount for certificates data
      - caddy-config:/config  # volume mount for configuration data
      - $PWD/Caddyfile:/etc/caddy/Caddyfile  # to use your own Caddyfile
#      - $PWD/log:/var/log  # bind mount for the log directory (optional)
      - /var/log/caddy:/var/log/caddy
      - $PWD/srv:/srv \  # bind mount to serve static sites or files (optional)
    environment:
#      - CLOUDFLARE_API_TOKEN=<token-value>  # Cloudflare API token (if applicable)
#      - DUCKDNS_API_TOKEN=<token-value>  # DuckDNS API token (if applicable)
      - CROWDSEC_API_KEY=<REDACED>
#      - NETCUP_CUSTOMER_NUMBER=<number-value> \  # Netcup customer number (if applicable)
#      - NETCUP_API_KEY=<key-value> \  # Netcup API key (if applicable)
#      - NETCUP_API_PASSWORD=<password-value> \  # Netcup API password (if applicable)
volumes:
  caddy-data:
    external: true
  caddy-config:

d. My Caddy config:

	debug # makes Caddy logs more detailed (optional)
	order crowdsec first # forces the CrowdSec directive to be executed first
	crowdsec {
		api_url http://REDACTED:8080 # it should point to your CrowdSec API (it can be a remote URL)
		api_key {env.CROWDSEC_API_KEY}
	}

	log {
		output file /var/log/caddy/access.log {
			roll_size 10mb
			roll_keep 20
			roll_keep_for 720h
		}
	}
}

#Other servers not relevant and redacted 

vc.public.tld {
        crowdsec
        log
        reverse_proxy https://vc.private.local {
                header_up Host vc.private.local
                header_up Origin vc.private.local
                transport http {
                        tls_insecure_skip_verify
                }
        }
}

5. Links to relevant resources:

Old nginx configuration:

server {
   location / {
      include /etc/nginx/conf.d/dynamicips;
      include /etc/nginx/conf.d/staticips;
      deny all;
      proxy_set_header Host "vc.private.local";
      proxy_set_header Origin "vc.private.local";
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-Server $host;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Authorization "";
      proxy_set_header Origin "";
      proxy_pass_header X-XSRF-TOKEN;
      proxy_ssl_verify off;
      proxy_pass https://vc.private.local;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "Upgrade";
      proxy_buffering off;
      proxy_send_timeout      300;
      proxy_read_timeout      300;
      send_timeout            300;
      client_max_body_size    1000m;
      proxy_redirect https://vc.private.local/ https://vc.public.tld/;
   }

   location /websso/SAML2 {
      include /etc/nginx/conf.d/dynamicips;
      include /etc/nginx/conf.d/staticips;
      deny all;
      sub_filter "vc.private.local" "vc.public.tld";
      proxy_set_header Host vc.private.local;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-Server $host;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_set_header Authorization "";
      proxy_set_header Origin "";
      proxy_pass_header X-XSRF-TOKEN;
      proxy_ssl_verify off;
      proxy_pass https://vc.private.local;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
      proxy_buffering off;
      proxy_send_timeout      300;
      proxy_read_timeout      300;
      send_timeout            300;
      client_max_body_size    1000m;
      proxy_ssl_session_reuse on;
      proxy_redirect https://vc.private.local/ https://vc.public.tld/;
  }

  server_name vc.public.tld;
    listen 443 http2 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/vc.public.tld/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/vc.public.tld/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
    if ($host = vc.public.tld) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

  server_name vc.public.tld;
  listen 80;
    return 404; # managed by Certbot
}

Howdy @vacum, welcome to the Caddy community!

This might be a bit complicated, but if you’ve achieved this in nginx with header manipulation, it should be doable in Caddy.

You’re on the right track. Looking at Module ngx_http_proxy_module, the proxy_set_header is a function that modifies the header going upstream to the proxied server. Reviewing reverse_proxy (Caddyfile directive) — Caddy Documentation, our equivalent in Caddy is indeed header_up.

Comparing between your Caddyfile and the nginx configuration you posted, it looks like the nginx has a few more you haven’t converted yet. But they follow the format of the ones you’ve already copied.

I can see you’ve already reviewed Global options (Caddyfile) — Caddy Documentation and set the debug option in your Caddyfile. That will help you as Caddy will emit the upstream roundtrip request logs, including a list of all the headers it received from client, headers sent upstream, and headers returned to client. This might also help you identify how and possibly why you’re being redirected.

This is pretty easy to do, logically speaking. You can use Request matchers (Caddyfile) — Caddy Documentation to filter for the IP ranges you want. You’re looking for either the remote_ip matcher, or the client_ip matcher if you have a trusted reverse proxy in front of Caddy at all.

Once you’re matching the desired traffic, you might either limit the reverse_proxy to the scope of the allowlist matcher, or you might use respond (Caddyfile directive) — Caddy Documentation to reject anyone not on the allowlist.

1 Like

Hello and thank you for the help. I managed to make vsphere run behind caddy :slight_smile:
I had to switch to running a manual installation of caddy where i added replace_response and caddy-crowdsec-bouncer to the binary.

I copied all the parameters from the nginx config to make sure i don’t miss anything. I’m not sure if i needed all the parameters i added :slight_smile:

The site looks like this in the caddyfile

vc.public.tld {
        #       list of allowed IPs to include in config
        #       include /etc/nginx/conf.d/dynamicips;
        #       include /etc/nginx/conf.d/staticips;

        @denied not client_ip 192.168.0.0/16 172.16.0.0/12 10.0.0.0/8 127.0.0.1/8 111.222.333.444/32
        abort @denied

        crowdsec
        log {
                output file /var/log/caddy/vc.log {
                        roll_size 10mb
                        roll_keep 20
                        roll_keep_for 720h
                }
        }
        replace https://vc.private.local https://vc.public.tld
        reverse_proxy https://vc.private.local {
                #               sub_filter "vc.private.local" "vc.public.tld";
                #               replace https://vc.private.local http://vc.public.tld
                #               proxy_set_header Host "vc.private.local";
                header_up Host vc.private.local

                #               proxy_set_header Origin "vc.private.local";
                header_up Origin vc.private.local

                #               proxy_set_header X-Real-IP $remote_addr;
                #               proxy_set_header X-Forwarded-Server $host;
                #               proxy_set_header X-Forwarded-Proto $scheme;
                #               proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

                #               proxy_set_header Authorization "";
                header_up Authorization ""

                #               proxy_set_header Upgrade $http_upgrade;

                #               proxy_set_header Origin "";
                header_up Connection "Upgrade"

                #               proxy_pass_header X-XSRF-TOKEN;
                header_up X-XSRF-TOKEN {>X-XSRF-TOKEN}

                #               proxy_redirect https://vc.private.local/ https://vc.public.tld/;
                header_down Location vc.private.local vc.public.tld

                #      sub_filter "vc.private.local" "vc.public.tld";
                #      proxy_set_header Origin "";

                #skip?
                #      proxy_http_version 1.1;
                #      proxy_buffering off;
                #      proxy_send_timeout      300;
                #      proxy_read_timeout      300;
                #      send_timeout            300;
                #      client_max_body_size    1000m;
                #      proxy_ssl_session_reuse on;

                transport http {
                        #                       proxy_ssl_verify off;
                        tls_insecure_skip_verify
                }
        }
}

In nginx i included configuration files that contained the allowed static and allowed dynamic IPs (a script updates the dynamic file and if any IPs change it reloads nginx)

I would like to do that in caddy.
Is it possible to to load the different lists of networks/IPs as parameters and use them like this?

        $localIPs = /path/to/localIPs
        $staticIPs = /path/to/staticIPs
        $dynamicIPs = /path/to/dynamicIPs
        @denied not client_ip $localIPs $staticIPs $dynamicIPs
        abort @denied

Remove those. They don’t do the right thing. You don’t need them anyway. Caddy passes through headers as-is by default.

You probably don’t need this.

You can simplify this to header_up Host {upstream_hostport} so you don’t need to repeat your domain. See reverse_proxy (Caddyfile directive) — Caddy Documentation

I think this is wrong, typically the Origin includes the scheme, like https://vc.private.local. See Origin - HTTP | MDN

You could if you use a plugin like GitHub - tuzzmaniandevil/caddy-dynamic-clientip: http.matchers.dynamic_client_ip matches requests by the client IP address, the ip addresses to match against is provided my a module that implements IPRangeSource paired with an “IP source” plugin which can pull from files.

Or you could use import if the file includes the leading Caddyfile config (can’t import partial lines of config, needs to be a full line). If your script took those 3 files then concatenated them together with @denied not client_ip as a prefix to that list of IPs, you could then import that into your Caddyfile.

But either way, it wouldn’t automatically reload, you’d have to trigger a reload with your script.

2 Likes

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.