Using Caddy Server as CORS Proxy for REST API

I’ve tried a few approaches to deal with CORS since REST API came out, including NGNIX and Traefik.

CORS is a standard used by all browsers to stop “cross-site scripting attacks”, problem is the REST API does not response to the added CORS headers when using REST API in a browser.

I tried Caddy over the weekend (since my containers got “lost” in 7.20beta a little bit ago) & need to “Proxy CORS” to test some RouterOS VSCode extensions I’m working in VSCode for Web – where “CORS rules” are enforced.

I don’t have time for a full write-up but want to save “what worked” someplace and see if the new forum works too…

I used 7.20beta2 for these test on RB1100AHx4, and “bridged VETH” to my LAN with dat-nat rules to port forward to container - this works will with default configuration. But how to wire up firewall and VETH is up to reader here. I’m mainly wanted to document Caddy /container setup as a CORS proxy for RouterOS – which is complex since RouterOS uses authentication which makes CORS even trickier…

Setting up Caddy as /container

:global caddydisk "disk1/caddyserver-root"

    # add VETH to use for caddy
/interface veth add address=192.168.88.7/24 gateway=192.168.88.1 name=veth-caddy

    # add VETH to bridge as LAN port 
/interface bridge port add interface=veth-caddy bridge=([/interface/bridge/find]->0) # if vlans, use pvid=<vlan>
    # do NOT set an IP address on router for VETH if bridged - none is needed

     # add the container 
/container add check-certificate=no interface=veth-caddy logging=yes root-dir=$caddydisk start-on-boot=yes remote-image=registry-1.docker.io/library/caddy:latest

    # for 7.20 beta, the check-certificate=no is required since builtin roots do not have docker's listed

Firewall Configuration

Caddy will listen on 192.168.88.7 on both 80 and 443 at this point on whatever LAN it’s bridged to. To write it from VPN or WAN, using a dst-nat is the approach I use

/ip firewall nat

add action=dst-nat chain=dstnat dst-port=443 protocol=tcp src-address=!192.168.88.7 to-addresses=192.168.88.7 to-ports=443

add action=dst-nat chain=dstnat dst-port=80 protocol=tcp src-address=!192.168.88.7 to-addresses=192.168.88.7 to-ports=80

     # note: it is important that you do not forward yourself too, why the !192.168.88.7 above

This forwards everything, so add more filters to above commands to limit if needed.

Also need to consider the ports used by RouterOS in configuration. To see what ports are used:

 /ip/service/print where name~("www-ssl|www") dynamic=no 

For example, you likely want Caddy listening on 80 and 443, but those are used by RouterOS. While possible, It may help to use different ports to keep things straight in your head.

Additionally, /ip/service for www & www-ssl let you set allowed-address which can augment the firewall.

Starting the Caddyserver

Now start the container:

/container/start caddy

It should be listening on http://192.168.88.7 and show some status page at Caddy’s address on LAN if all is working.

TLS Certificates

Caddy automatically deals with getting LE – it’s kinda it’s “big pitch” – so no configuration to get valid certs should be required. So above https://192.168.88.7 should work too…

And for CORS support it does *need a valid SSL certificate otherwise it’s still CORS failure before even getting to RouterOS.

For a domain name, /ip/cloud/ddns can be used or if you already have a domain that points to RouterOS device that works too. In example config using “0000000000.sn.mynetname.net” as DDNS name, with the RouterOS REST API being at 192.168.88.1.

If want to use MirkoTik’s DDNS, enable it via:

/ip/cloud/set ddns-enabled=yes

and after a bit, you can get the DDNS name for the Caddyfile below

:put [/ip/cloud/get dns-name ]

0000000000.sn.mynetname.net

Caddyfile for CORS Proxy

You’ll finally need to add the Caddyfile to /etc/caddy/Caddyfile, replacing 192.168.88.1 that represents the RouterOS device you want to add CORS headers via Caddy. And the DDNS name too.

Once edited, you can use

/container/shell caddy

then inside the container use:

vi /etc/caddy/Caddyfile

and append the configuration with below - if you want Caddy to do other things that’s fine or remove the default 80 file-server that’s okay. But this part is in the Caddyfile is what’s need for CORS with RouterOS REST API:

http://0000000000.sn.mynetname.net {
    redir https://0000000000.sn.mynetname.net{uri}
}

https://0000000000.sn.mynetname.net {
    reverse_proxy http://192.168.88.1 {
        header_up Host {http.request.host}
    }

    header {
        Access-Control-Allow-Origin "{http.request.header.Origin}"
        Access-Control-Allow-Credentials "true"
        Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
        Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With"
        Access-Control-Max-Age "100"
        Vary "Origin"
    }

    @options method OPTIONS
    respond @options 204
}

Once replaced, you can restart the container:

/container stop caddy
:delay 5s
/container start caddy

At this point any request to https://0000000000.sn.mynetname.net will add headers (and respond to OPTIONS preflight) for CORS, proxying REST API to http://192.168.88.1 (since it’s on same host no need for SSL, assuming /ip/service www is restricted appropriately (i.e. to just the Caddy container IP), www-ssl does not need to be enabled since Caddy will proxy that too using the above configuration.

Viewing /container logs

This rather complex command will “tail” the msg from Caddy’s JSON logs - which are particular hard to read in WinBox):

/log print follow proplist=message where topics=container [:put ([:deserialize from=json [:pick $message 6 9999]]->"msg")]

Removing the ->"msg" after the :deserialize array, will show all attributes in routeros array print style:

/log print follow proplist=message where topics=container [:put ([:deserialize from=json [:pick $message 6 9999]])]

This will keep running and show the “extract” from the container logs. You can remove the “follow” in above to see the past logs.

If you have multiple containers, well, you may see JSON errors (since those would not be JSON).

1 Like