Community discussions

MikroTik App
 
User avatar
Amm0
Forum Guru
Forum Guru
Topic Author
Posts: 3253
Joined: Sun May 01, 2016 7:12 pm
Location: California

"Mginx" Container - Reverse Proxy for CORS & X.509 support using Nginx

Mon Dec 05, 2022 9:44 pm

I've been trying to Mikrotik REST via browser-side JavaScript. See So RouterOS and Observable meet in a JavaScript bar... for what I'm trying to. But calling JavaScript from a browser doesn't generally work, without server-side support - since a web browser's JavaScripts requires the server to support sending specific headers to incoming REST API calls. This is called CORS. Issue is that Mikrotik's internal web server send any response to an OPTIONS request that checks, and there is nothing you can to inside browser-side JavaScript to "override CORS" or "skip auth checks" etc.

The "classic" approach to enabled REST web service that doesn't support CORS is to use a proxy server that responds with the needed headers (e.g. responds that contain a few headers starting with "Access-Control-Request-"). The underlying issue with CORS and using NGINX reverse proxy to solve is discussed here: https://blogs.perficient.com/2021/02/10 ... -for-cors/.

While I wait, perhaps forever, for Mikrotik to add CORS support to the REST API...

One method avoid the wait is to use a container on the Mikrotik with the REST API that does this proxy. That what I'm trying here. So the rest of the post documents how to create an NGINX docker-like container that acts as a "reverse proxy" to the real Mikrotik HTTPS server using Mikrotik container supporting. See https://www.docker.com/blog/how-to-use- ... ker-image/ which is what this container/code below is largely based upon, with build.sh to deal with using it on a Mikrotik.

I write this up assuming the basics of containers are somewhat understood & generally understand NGINX/"reverse proxies" - otherwise the explanation be long.... And stuff like you need to have the web server enabled/working, firewall, IP addresses, all set right to use a proxy in the first - that isn't exactly covered here. The build.sh script tries to do everything but you need SSH enabled for your routeros account for that work with the build.sh script (see below).

So if you need NGINX, this seems to work for me. The config is specific to CORS and X.509 client authentically, my rough needs – but any nginx.conf could be used for your needs. It's a bit complex, since it actually tries to passthrough variables from build script to runtime, which isn't that easy in Docker as turns out. I tried to document the various scripts, instead of this post. So if interested, read the code before use! Up to you how you use it, but so no warranties here.

To use, you should just need to put the 3 files: build.sh, Dockerfile, and nginx.conf into a new directory. Then edit the top part of "build.sh" as needed, ideally having SSH enabled to automatically deploy/run the container on a specific Mikrotik by setting the "SSHHOST" should be all that's needed (assuming you have a working REST enabled, firewall allows, likely more assumptions,... )

By default we use a 169.254 address for the container – since it's a proxy, we want to force using it through a dst-nat rule for additional safety since 169.254.x.x should generally not be routable. See build.sh below for various commands used to setup this up on the Mikrotik AFTER the container build.

The container is named "mginx" as pun of the embedded Nginx web server used. Change in build.sh as desired.

Here are the three files needed, with the build.sh script creating the self-signed certs to use.


Dockerfile to build NGINX container for Mikrotik

This must be named Dockerfile (no extension) and should live next to build.sh and nginx.conf files.

This is what docker "builds", it's based on the official nginx docker images, which provide hides the actually installation/setup of nginx. FWIW, "ARG" are provided a build time while "ENV" are available inside the container, so we link them in the Dockerfile - otherwise pretty standard.
FROM nginx:alpine

# NOTE: Dockerfile requires key/cert for proxy webserver.
#       ./build.sh run from the same directory will generate them
#       But can be generated using the following at docker build terminal:
# openssl req -x509 -newkey rsa:4096 -keyout self.key.pem -out self.cert.pem -sha256 -days 1024 -nodes -subj "/C=AQ/O=Unsecured Worldwide/OU=Self Signed/CN=router.lan"

# "build time" arguments to override container image default ENVs
ARG defaultProxyPort=6443
ARG defaultContainerGateway=169.254.8.1
ARG defaultRouterHttpsPort=443
ARG defaultProxyHostname=router.lan
ARG defaultX509AuthMode=off
ARG defaultBase64AuthBypass="b@se64encod3dUser:P@ssw0rd="

# "runtime" arguments, this can be provided in the container at launch
ENV ROUTERHOST=$defaultContainerGateway
ENV ROUTERHTTPSPORT=$defaultRouterHttpsPort
ENV OURPROXYPORT=$defaultProxyPort
ENV OURPROXYHOST=$defaultProxyHostname
ENV BASE64AUTHBYPASS=$defaultBase64AuthBypass

# Use X509 authentication?
# e.g. sets nginx's ssl_verify_client, values: on | off | optional | optional_no_ca
ENV X509AUTHMODE=$defaultX509AuthMode


# configuration file used by proxy 
#   ... note it goes to ".../templates", but a NGINX script parses it to
#       /etc/nginx/conf.d/default.conf 
#   ... but this is how we can use any ENV defined in this Dockerfile
#       inside the nginx configuraiton (which does NOT support environment vars) 
COPY nginx.conf /etc/nginx/templates/default.conf.template

# SSL server certificates - these are need to enabled SSL, which is required by CORS
#   ... since we proxy SSL, we either need to export RouterOS's key/cert to use
#       or use self-signed ones & trust them in the browser's computer,
#       or do more fancy stuff like certbot in a container, etc.  #self-signed works fine
COPY self.cert.pem /etc/ssl/default.crt
COPY self.key.pem /etc/ssl/default.key

# This is not needed unless you modify the config to use X.509 authentication
# e.g. via X509AUTHMODE=on - add your own file, and change "self.cert.pem" to use a different
# root CA as the trusted source for verifying an *client* X.509 auth recieved.
# NOTE: 
COPY self.cert.pem /etc/ssl/certauth.ca.crt

# Copy the "public" web pages, from the build's "public" directory, disabled by default
# COPY public/ /home/www/public/

# Not sure RouterOS uses this, but tells the container system the proxy port 
# we'll be listening on.
EXPOSE $defaultProxyPort 

# ENTRYPOINT comes from NGINX parent - normally there'd be one - but not an error here.




Build Script (./build.sh)

This must be named build.sh and should live next to Dockerfile and nginx.conf files. It can be called on Mac/Linux, using "./build.sh" (you may have to change permissions to allow +x, depending on OS).
#!/bin/sh

# Container name
IMAGENAME=mginx
echo "Container name (e.g. .tar name, without the .tar) = $IMAGENAME"

# Container platform to build
BUILDPLATFORM=linux/arm/v7
echo "What platform to build? = $BUILDPLATFORM" 

# Build copies container TAR to Mikrotik and configures it
# and uses SSH/SCP to do it.  Fromat is <username>@<routeros_ip>.
# SSH must be enabled on RouterOS for this work.
# If a SSH key is defined for the user, no password is required
# Otherwise, the build will prompt for credentials for upload & config
SSHHOST=admin@router.lan
echo "SSH/SCP user@host = $SSHHOST"

# When copied, where on RouterOS file system (path only)?
TARDEST=sata1-part1
echo "Container build (.tar) will be copied to: $TARDEST/$IMAGENAME.tar"

# An subnet is used for the container. Typically 24 (for a /24)
SUBNETSIZE=24
echo "Container network using a /$SUBNETSIZE" 

# Container's IP address
CONTAINERIP=169.254.8.2
echo "Container IP is: $CONTAINERIP/$SUBNETSIZE" 
#    ... Must be same subnet (based on SUBNETSIZE) as ROUTERHOST below 

# IP address of router running the container
ROUTERHOST=169.254.8.1
echo "Container Gateway (Hosting RouterOS) IP is: $ROUTERHOST/$SUBNETSIZE" 
#    ... Must be same subnet (based on SUBNETSIZE) as CONTAINERIP above
#         & both that subnet must NOT overlap any existing subnet on router

# HTTPS port configurated on Mikrotik the proxy will use
ROUTERHTTPSPORT=443
echo "Hosting RouterOS HTTPS URL: https://$ROUTERHOST:$ROUTERHTTPSPORT" 

# Proxy server's HTTP hostname
OURPROXYHOST=router.lan
echo "Proxy Server Name: $OURPROXYHOST/$SUBNETSIZE" 

# Proxy server's listen port for proxy requests
OURPROXYPORT=6443
echo "Proxy Server Address (may need dest-nat rule on RouterOS): https://$OURPROXYHOST:$OURPROXYPORT" 

# When creating the container's network, what virtual interface name to use
NETIFACE=vethMginxProxy
echo "Container will create/use virtual network interface: $NETIFACE" 

# SSH & SCP are used to automate deployment and configuration
SSHCMD=ssh
SCPCMD=scp
# but to disable, un-comment below
#SSHCMD=echo
#SCPCMD=echo

# Use X509 Authentication
# e.g. sets nginx's ssl_verify_client, values: on | off | optional | optional_no_ca
X509AUTHMODE=off

# Self-signed Validity
# When generating self-signed certificate, the number of days they should be valid
SELFSSLDAYS=1024

# This isn't used unless the nginx.conf is explictly changed, but
# it's the Base-64 version of user:password for your router.  This is fake.
# It's here to avoid needing to change container code to use it if desired.
BASE64AUTHBYPASS="baae64eec0d3d48e57a00902d="

### START BUILD
# NOTE: All build config variables should be assigned above, and only _used_ below...

# Detect actual router's SSL port (TODO: override the configured one if got VALID one...)
# DETECTED_SSLPORT=`$SSHCMD $SSHHOST ":put [/ip/service/get www-ssl port]"` 
echo "Proxy is using $ROUTERHTTPSPORT this needs to match your www-ssl port on RouterOS!"


# Generate self-signed key and certificate for container web server
#    ... typically this is only done once, you can comment out if you want to rebuild and use same certificate
openssl req -x509 -newkey rsa:4096 -keyout self.key.pem -out self.cert.pem -sha256 -days $SELFSSLDAYS -nodes -subj "/C=AQ/O=Unsecured Worldwide/OU=Self Signed/CN=$OURPROXYHOST"

#    ... HINT: if a build is being deployed to same server in future, you may want to comment out the above
#              as it will use the already generated keys = the client-side trust doesn't need to change
#              otherwise, you will have to "re-trust" the newly generated self-signed cert on your PC before using CORS again


# These create potential client/browser-side X509 authentication cert that can be used to access REST
#   Note: A password is still required by default WITH cert.  See nginx.conf for details ONLY require X509.
#         X509AUTHMODE must be "on" for these to have any effect.
# X509AUTHMODE=on   
# create client certificate request
# openssl req -newkey rsa:4096 -keyout client.key.pem -out client.csr.pem -nodes -days $SELFSSLDAYS -subj "/C=AQ/O=Unsecured Worldwide/OU=Self Signed/CN=X509 Client Access to $OURPROXYHOST"
# sign request using server's self-signed SSL certificate as the "CA", which is what to this instance signed client cert = authorized
# openssl x509 -req -in client.csr.pem -CA self.cert.pem -CAkey self.key.pem -out client.cert.pem -set_serial 01 -days $SELFSSLDAYS
# the PEM file can be imported in the local system (or another system) to beable to access the proxy with X509
# EXAMPLE:  This converts the generated keys into a PKCS12 file that can be imported to a PC - but requires a passphrase
#           & asking for one during a build may be confusing.  But uncomment, and load in Certificate Keychain and browser can use it.
#openssl pkcs12 -export -clcerts -in client.cert.pem -inkey client.key.pem -out client.p12


echo "** Starting Docker Build **"

# Using --build-arg to convert the shell env vars into docker build ARG values used by Dockerfile...
#  n.b. which then convert back to env vars inside the container,
#       so default settings can be from here (buildtime) and built-in to image
#       or changed at runtime inside container's settings later

# Build the container for platform
docker buildx build --platform $BUILDPLATFORM \
--build-arg defaultContainerGateway=$ROUTERHOST \
--build-arg defaultRouterHttpsPort=$ROUTERHTTPSPORT \
--build-arg defaultProxyHostname=$OURPROXYHOST \
--build-arg defaultProxyPort=$OURPROXYPORT \
--build-arg defaultX509AuthMode=$X509AUTHMODE \
--build-arg defaultBase64AuthBypass=$BASE64AUTHBYPASS \
-t $IMAGENAME . 

echo "** Docker Build Completed **"

# Save and generate build as .tar file
docker save $IMAGENAME > $IMAGENAME.tar    
echo "Container image, $IMAGENAME.tar, saved locally" 
pwd 

# Copy the tar to the router
echo "Copy to RouterOS..."
SCP_COPY_CMD="$IMAGENAME.tar $SSHHOST:$TARDEST/$IMAGENAME.tar"
echo $SCP_COPY_CMD
$SCPCMD $SCP_COPY_CMD

# Create the network interface and firewall rules
echo RouterOS configuration...

# remove any veth assocated with container
SCMD_RMNET="{ /interface/veth remove [find comment~\"$IMAGENAME\"]; /ip/address remove [find comment~\"$IMAGENAME\"] }" 
echo $SCMD_RMNET
$SSHCMD $SSHHOST "$SCMD_RMNET" 

# add a veth to use for container
SCMD_MKNET="/interface/veth add address=$CONTAINERIP/$SUBNETSIZE gateway=$ROUTERHOST comment=\"$IMAGENAME\" name=\"$NETIFACE\" }; /"
echo $SCMD_MKNET
$SSHCMD $SSHHOST "$SCMD_MKNET" 

# add IP address to same veth for router
SCMD_MKIP="/ip/address add address=$ROUTERHOST/$SUBNETSIZE interface=\"$NETIFACE\" comment=\"$IMAGENAME\" }; /"
echo $SCMD_MKIP 
$SSHCMD $SSHHOST "$SCMD_MKIP"
 
# remove any containers of our type - we only want one
SCMD_RMDOCK="/container { :foreach i in=[find comment~\"$IMAGENAME\"] do={stop \$i; :delay 10s; remove \$i }}; /"
echo $SCMD_RMDOCK
$SSHCMD $SSHHOST "$SCMD_RMDOCK"

# add a new container using this build and start it
SCMD_MKDOCK="/container { add file=$TARDEST/$IMAGENAME.tar logging=yes start-on-boot=yes interface=\"$NETIFACE\" comment=\"$IMAGENAME\"; :delay 10s; start [find comment=\"$IMAGENAME\"]; }; /"
echo $SCMD_MKDOCK 
$SSHCMD $SSHHOST "$SCMD_MKDOCK"

# re-create a NAT dst-nat rule that provides access to proxy
SCMD_NATRULE="/ip/firewall/nat { remove [find comment~\"$IMAGENAME\"]; add action=dst-nat chain=dstnat dst-port=$OURPROXYPORT protocol=tcp to-addresses=$CONTAINERIP to-ports=$OURPROXYPORT comment=\"$IMAGENAME\" }"
echo $SCMD_NATRULE
$SSHCMD $SSHHOST "$SCMD_NATRULE"

echo "** END **"



NGINX configuration (must be named ./nginx.conf)

The file must be named nginx.conf, and placed next to build.sh and Dockerfile.

This is uses NGINX container's "template" feature, so that environment variables can be passed into the NGINX configuration file (which does NOT support env vars). See https://www.docker.com/blog/how-to-use- ... ker-image/ for details. It just needs to be placed next to the Dockerfile at build, and will go to right spot in container image. Again the shell/.sh variables going to ARG to build to ENV to back to NGINX-provided shell script inside container that process the variables inside the Dockerized nginx.conf is part of the magic in this.

Now how Nginx configuration works, up to you. This proxies CORS and could support X.509 certificates with minor tweaks in build process. But if you have other needs for a web server on Mikrotik container AND know something about nginx, change at well. In fact, most interesting here is the approach to the variables that somehow make it from the building computer to runtime, with being able to be customized at any of those points. For example, overriding later in RouterOS using env vars within /container
###
# NGINX Proxy for Mikrotik RouterOS to add X.509 certs & CORS supports
###

# Technically, this is a "template"...
#    the $ { stuff } are Docker environment ENV variables that
#    can be provided at *runtime*, this happens via script
#    included by NGINX that parses this code before using it.

server {
   # Proxy all RouterOS traffic recieved on ENV:OURPROXYPORT (6443 default)
   listen ${OURPROXYPORT} ssl;

   ### X.509 Client Authentication Support ###
   
   # This is disabled by default, but tested and "plumb'ed".
   # If a *client* browser using this proxy has a X.509 cert install
   # that was *signed* by the CA referenced in ssl_client_certificate,
   # that can be used to authenticate to this proxy, thus *adding* security.
   # If you trust your certificate setup, the basic auth needed to use the
   # REST API can be provided using proxy_set_header - so no password needs 
   # be used with CORS, only the X.509 cert.  Instead, after auth, this proxy
   # can add the needed fixed username/password before  
  
   ssl_client_certificate /etc/ssl/certauth.ca.crt;
   ssl_verify_client ${X509AUTHMODE};
   
   #   HINT: Ideally replace the certauth.ca.crt file as part of Dockerfile. 
   #           The CA KEY file is NOT needed, just a PEM version when you use a "real" one.
   #           But since we have the server's self-signed certificate, that can be the CA to
   #           to sign client certs & auth them - this could be done via /container/shell on router
   #           as one workflow to get clients X509 certs.  Using /certificate on the Mikrotik
   #           likely be better, but even more complex to explain.  Thus just a "HINT" here on options.
   #   NOTE: The default is to use the self-generated SSL server key as the CA used to verify client X509.
   #           This is done by copying the key.pem to two locations, but for X509 using a different CA
   #           just add a file to build that points to certauth.ca.crt.  

   ### X.509 Passthrough Authentication
   # Is commented-out & disabled – since you need to customize if used...
   # "X.509 passthrough authentication" also this proxy to provide a username/password 
   # to RouterOS REST API on behalf of the user.  Since it a fixed, you'd want to 
   # really think about the security model before user (e.g. X.509 is required & working): 
    
   # proxy_set_header Authorization 'Basic ${BASE64AUTHBYPASS}';

   # WARNING: Using proxy_set_header Authorization is automatically providing any proxy'ed call to be
   #          authenticated WITHOUT a password.  If you have "ssl_verify_client on", this container
   #          verifies a valid cert BEFORE automatically providing the password - that how this is to be used.
   # HINT: You'll also need to use something to find/encode Authentication value in header above, like Postman.
   
   # Use SSL key and cert installed by Dockerfile at *build* time
   # ./build.sh that creates the .tar image, generates self-signed ones by default
   # To use your own, likely better to reference them in Dockerfile to these names: 
   ssl_certificate /etc/ssl/default.crt;
   ssl_certificate_key /etc/ssl/default.key;

   # Similarly, this should match the CN of the SSL certificate.  
   # For self-signed, it does & router.lan is used in the RouterOS default config
   server_name ${OURPROXYHOST};

   # This is just notes.  Logging goes stdout/stderr by default, which on RouterOS
   # is preferred at this point to going to container disk.  In theory, the log directories
   # should be mounted in the Dockerfile/container, but RouterOS does not support.
   # Leave commented out for now:
   #access_log   /var/log/nginx/nginx.vhost.access.log;
   #error_log    /var/log/nginx/nginx.vhost.error.log;
   
   # For a static web site that is not proxied, add files via Dockerfile. 
   # Disabled, mainly to test & requires changing the "location /" to "location /rest" or etc etc:
   #root /home/www/public;
   #autoindex on;
   #index index.html;
   # ... TODO: the above can be used to include an example JS code that uses the proxy in future

   # For all request, we just add CORS headers and potentially more.  While targeted
   # at /rest, we just always add CORS to everything going through the proxy.
   # NOTE: the proxy only proxies to the local router running the container,
   #       and NOT just any server (although that be possible with different config)
   
   # for the actual root, just redirect to real root
   #   e.g. logos/graphics would never need CORS
   location = / {
      proxy_pass https://${ROUTERHOST}:${ROUTERHTTPSPORT}; 
   }
   location / {
      # This is what a web browser JavaScript needs to see to use RouterOS REST API,
      # which is why there is this container in the middle...
      add_header 'Access-Control-Allow-Origin' '*' always;
      add_header 'Access-Control-Allow-Methods' 'GET, POST, PATCH, PUT, DELETE, OPTIONS' always;
      add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always;
      add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
      # since CORS needs OPTIONS
      # we provide a generic answer, that yes we support it.
      if ($request_method = 'OPTIONS') {
         add_header 'Access-Control-Allow-Origin' '*' always;
         add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
         add_header 'Access-Control-Max-Age' 1728000;
         add_header 'Content-Type' 'text/plain; charset=utf-8';
         add_header 'Content-Length' 0;
         return 204;
      }
      # And as a proxy, we need to actually do that.  We can add headers OUTBOUND
      # so theoritically the RouterOS knows the request was proxies.  But ROS doesn't care AFAIK.
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    
      # This is what does all the work.  It take any request recieved here, and just passes along
      # all the headers/data to the "real" RouterOS web server.  This is configuration at 
      # runtime via ENV vars, but using 172.28.1.1:443 is default 
      proxy_pass https://${ROUTERHOST}:${ROUTERHTTPSPORT};

      # ... again the $ { stuff } can be provided in environment at *runtime* on Mikrotik container,
      #     (or buildtime in Dockerfile) 
   }
}
Hope this is useful to someone. But works for me.


edit: It's Nginx NOT Ngnix - fixed for clarity
edit 2: X.509 not X506
 
xsmael
just joined
Posts: 10
Joined: Wed May 15, 2019 6:33 pm

Re: "Mginx" Container - Reverse Proxy for CORS & X.509 support using Nginx

Tue Dec 20, 2022 2:38 pm

Hello, I have a use case where i need a web app from my browser to access the mikrotik REST API which is available since v7. i could setup a small server to handle that, but my goal is to make a very easy and light solution for the end user, So ideally i wont put up a server. So i faced the CORS issue and found your post.

If i understand well, your solution gets embedded directly on to the mikrotik, and will make the REST API available from any Origin ?
I'm also surprised, but it looks like you have also embedded a php web server on the mikrotik, is that so ? if it is the case does that mean we can also run web apps directly from the mikrotik ?
 
User avatar
Amm0
Forum Guru
Forum Guru
Topic Author
Posts: 3253
Joined: Sun May 01, 2016 7:12 pm
Location: California

Re: "Mginx" Container - Reverse Proxy for CORS & X.509 support using Nginx

Tue Dec 20, 2022 4:18 pm

If i understand well, your solution gets embedded directly on to the mikrotik, and will make the REST API available from any Origin ?
Correct. Technically, It takes the origin it got from a client, and returns the same origin as part of the CORS headers – but functions same as "any". (You can also add X.509 client authentications too but you have to change the config and add certificates)
I'm also surprised, but it looks like you have also embedded a php web server on the mikrotik, is that so ? if it is the case does that mean we can also run web apps directly from the mikrotik ?
You extend this to do that. I'm not sure the needed PHP runtime is installed by default. But if can build your own docker image that does include it. I'm not the PHP expert, but typically ngnix enables PHP via their "fastcgi" plugin. See https://www.nginx.com/resources/wiki/st ... s/phpfcgi/ . You'd have to change the Dockerfile to add PHP to linux, add files with PHP files to the Dockerfile, and change the ngnix.conf have to config that uses the "fastcgi" plugin to call your PHP.

For example, to add package to the Dockerfile for PHP, you'd need the following code after the "ENV" in the Dockerfile:
RUN apk add --no-cache \
  curl \
  php81 \
  php81-ctype \
  php81-curl \
  php81-dom \
  php81-fpm \
  php81-gd \
  php81-intl \
  php81-mbstring \
  php81-mysqli \
  php81-opcache \
  php81-openssl \
  php81-phar \
  php81-session \
  php81-xml \
  php81-xmlreader 
Plus your PHP files need to have some "COPY" lines in Dockerfile to add your code. Finally, the ngnix.conf need to have a sections to route the URL to the PHP code (and some wildcard route that actually invoke the PHP runtime) - NGINX docs likely explains this better since I don't regularly use PHP.

Same approach work for nodeJS or Python. In larger environment, it's pretty typically to use NGINX (or Apache) as a "frontend" web server that proxies to internal node or python to apply filters, static content, and avoid needing SSL certs inside a node/PHP/python web server itself. So, in theory, nginx could be the "frontend" to all of them at the same time if you add the right package/code.

In my use case, I just have a hosted static HTML file with JavaScript that calls the Mikrotik REST API, so all I needed was NGINX to proxy the request and add CORS headers on the response.
 
xsmael
just joined
Posts: 10
Joined: Wed May 15, 2019 6:33 pm

Re: "Mginx" Container - Reverse Proxy for CORS & X.509 support using Nginx

Tue Dec 20, 2022 10:15 pm

Thank you very much! you are such a genius! I gotta try all that.

But for now, i'm trying to make the reverse proxy work. I'm working from a windows laptop, so had to activate WSL, and install ubuntu from it, install docker and all the needed after quite some trouble (damn windows) i got everthing right, but running the build.sh seems not go completely perfect, here is my output:
Container name (e.g. .tar name, without the .tar) = mginx
What platform to build? = linux/arm/v7
SSH/SCP user@host = admin@wididi.bf
Container build (.tar) will be copied to: sata1-part1/mginx.tar
Container network using a /24
Container IP is: 169.254.8.2/24
Container Gateway (Hosting RouterOS) IP is: 169.254.8.1/24
Hosting RouterOS HTTPS URL: https://169.254.8.1:8383
Proxy Server Name: wididi.bf/24
Proxy Server Address (may need dest-nat rule on RouterOS): https://wididi.bf:6443
Container will create/use virtual network interface: vethMginxProxy
Proxy is using 8383 this needs to match your www-ssl port on RouterOS!
.....+.......+...+..+....+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*.....+....+..+.+.........+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*.....+......+...+..........................+....+...........+......+....+.....+.........+....+...+....................+.............+..+....+...+............+..................+..+.........+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
..........+...+.......+..+.........+....+...+............+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*.........+.........+...+...+...........+...+.......+........+.+..+...+.+...........+...+....+..+.+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*..+............+.........+...+.+......+........+......+.+.........+..+...+.........+......+...............+...................+..+.........+.......+..+.+....................+.......+...+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
-----
** Starting Docker Build **
[+] Building 34.7s (10/10) FINISHED
 => [internal] load build definition from Dockerfile                                                               2.1s
 => => transferring dockerfile: 32B                                                                                0.0s
 => [internal] load .dockerignore                                                                                  3.0s
 => => transferring context: 2B                                                                                    0.0s
 => [internal] load metadata for docker.io/library/nginx:alpine                                                   11.3s
 => [internal] load build context                                                                                  1.4s
 => => transferring context: 5.36kB                                                                                0.0s
 => [1/5] FROM docker.io/library/nginx:alpine@sha256:dd8a054d7ef030e94a6449783605d6c306c1f69c10c2fa06b66a030e0d1d  0.0s
 => CACHED [2/5] COPY nginx.conf /etc/nginx/templates/default.conf.template                                        0.0s
 => [3/5] COPY self.cert.pem /etc/ssl/default.crt                                                                  2.9s
 => [4/5] COPY self.key.pem /etc/ssl/default.key                                                                   3.6s
 => [5/5] COPY self.cert.pem /etc/ssl/certauth.ca.crt                                                              3.7s
 => exporting to image                                                                                             7.0s
 => => exporting layers                                                                                            5.1s
 => => writing image sha256:7019459ea75ed905536bc57f65c2a3242445985af96900c49b00b53b81fd6b60                       0.2s
 => => naming to docker.io/library/mginx                                                                           0.2s
** Docker Build Completed **
Container image, mginx.tar, saved locally
/mnt/c/Users/ECODEV INT/Documents/@ECODEV/Hotspot business/Reverse Proxy
Copy to RouterOS...
mginx.tar admin@wididi.bf:sata1-part1/mginx.tar
admin@wididi.bf's password:
scp error: sata1-part1/mginx.tar: bad path!
exec request failed on channel 0
RouterOS configuration...
{ /interface/veth remove [find comment~"mginx"]; /ip/address remove [find comment~"mginx"] }
admin@wididi.bf's password:
/interface/veth add address=169.254.8.2/24 gateway=169.254.8.1 comment="mginx" name="vethMginxProxy" }; /
admin@wididi.bf's password:
/ip/address add address=169.254.8.1/24 interface="vethMginxProxy" comment="mginx" }; /
admin@wididi.bf's password:
/container { :foreach i in=[find comment~"mginx"] do={stop $i; :delay 10s; remove $i }}; /
admin@wididi.bf's password:
syntax error (line 1 column 12)
/container { add file=sata1-part1/mginx.tar logging=yes start-on-boot=yes interface="vethMginxProxy" comment="mginx"; :delay 10s; start [find comment="mginx"]; }; /
admin@wididi.bf's password:
syntax error (line 1 column 12)
/ip/firewall/nat { remove [find comment~"mginx"]; add action=dst-nat chain=dstnat dst-port=6443 protocol=tcp to-addresses=169.254.8.2 to-ports=6443 comment="mginx" }
admin@wididi.bf's password:
** END **
Now the two last commands have failed, i don't know why. I also update the domain (router.lan) to what i had set up before for my hotspot.
In your script there is line that says: "Proxy Server Address (may need dest-nat rule on RouterOS):" how do i know wether i actually need it or not ?
And how do i check to make sure that the proxy is running properly.
Thanks and sorry if the answer to these questions seem quite obvious, part of this is sill obscure to me, but i'm trying to cope!
 
User avatar
Amm0
Forum Guru
Forum Guru
Topic Author
Posts: 3253
Joined: Sun May 01, 2016 7:12 pm
Location: California

Re: "Mginx" Container - Reverse Proxy for CORS & X.509 support using Nginx

Tue Dec 20, 2022 10:30 pm

WSL should work fine here. Certainly better than cygwin etc. But for build.sh I really only tested using on a Mac, but should be using standard Bourne shell things that work fine in WSL.

You may have to change the path in the build.sh "sata1-part1" is what my RB1100AHx4 in v7.7rc uses. So I think the script goes south from there. So in the build.sh script, you'd need to change "TARDEST" to the actual disk that exists on the Mikrotik. Typically this would be disk1, but in v7.7+ it could be a few things. You likely don't want to use a / in front of the TARDEST if I recall.

I start by looking to see if the container says "started" after the build script. The Mikrotik changes to wire it up may vary - while I tried to be generic, I kinda shared my specific config needs.

Side note, if you create a key-pair for your user, the script would skip asking for your password. See https://wiki.mikrotik.com/wiki/Use_SSH_ ... key_login)
 
xsmael
just joined
Posts: 10
Joined: Wed May 15, 2019 6:33 pm

Re: "Mginx" Container - Reverse Proxy for CORS & X.509 support using Nginx

Wed Dec 21, 2022 3:34 pm

Thanks for your precious support!

I realised that what i was trying to do was hardly possible :( But i've learned a lot along the way. I'm using a MIPSBE architecture mikrotik(RB951Ui2Hnd, but was even targetting much cheaper hAP lite), and containers only work on ARM and x86 :facepalm: I was trying to go too fast. I was trying to design a cheap and compact solution that can be easily replicated for that i needed the REST API that came in routeros v7 and the possibility to allow cross origin requests, for wich the the reverse proxy was supposed to be the solution, but then i would need a higher end device, or buy routerOS License4 with ARM based SBC like RaspberryPi and these don't generally have a good WiFi radio, so would have to invest in that too and will quicly run out of the ideal budget for this solution. I'll keep searching to find a way out.

There was an interesting discussion about here (viewtopic.php?t=187595) about container support for MIPSBE and various use cases, but clearly seems like Mikrotik is not willing to go that route.

Regards.
 
User avatar
Amm0
Forum Guru
Forum Guru
Topic Author
Posts: 3253
Joined: Sun May 01, 2016 7:12 pm
Location: California

Re: "Mginx" Container - Reverse Proxy for CORS & X.509 support using Nginx

Wed Dec 21, 2022 4:45 pm

I realised that what i was trying to do was hardly possible :(

There was an interesting discussion about here (viewtopic.php?t=187595) about container support for MIPSBE and various use cases, but clearly seems like Mikrotik is not willing to go that route.
We have a lot of MIPSBE devices in the field, so know the pain – they also don't support ZeroTier either on MIPSBE which actually my bigger grip.

At least I know this builds under Windows+WSL+Ubuntu – thanks for testing that. And MIPSBE would explain your error messages: first error was the disk (way too small for anything) and 2nd error was "/container" doesn't exist – both were valid errors actually.
 
xsmael
just joined
Posts: 10
Joined: Wed May 15, 2019 6:33 pm

Re: "Mginx" Container - Reverse Proxy for CORS & X.509 support using Nginx

Wed Dec 21, 2022 4:58 pm

We have a lot of MIPSBE devices in the field, so know the pain – they also don't support ZeroTier either on MIPSBE which actually my bigger grip.
Yeah i get this, i only thought i'd get a quick and easy solution, but it's not for today. ZeroTier support would also be a game changer.

At least I know this builds under Windows+WSL+Ubuntu – thanks for testing that.
You're most welcome!

MIPSBE would explain your error messages: first error was the disk (way too small for anything) and 2nd error was "/container" doesn't exist – both were valid errors actually.
Exactly, i only figured that out quite late ^^' the real issue is MIPSBE support, about the storage, we could make a very slim container for one specific purpose, and some low cost RouterBords still have 64MB of storage, which would be okay for the job.
 
User avatar
normis
MikroTik Support
MikroTik Support
Posts: 26322
Joined: Fri May 28, 2004 11:04 am
Location: Riga, Latvia

Re: "Mginx" Container - Reverse Proxy for CORS & X.509 support using Nginx

Thu Dec 22, 2022 9:45 am

Container on MIPS is not out of spite, it is useless too - not many container images exist for MIPS architecture. You may get the container package, but what to do with it, without images?
 
User avatar
Amm0
Forum Guru
Forum Guru
Topic Author
Posts: 3253
Joined: Sun May 01, 2016 7:12 pm
Location: California

Re: "Mginx" Container - Reverse Proxy for CORS & X.509 support using Nginx

Fri Dec 23, 2022 12:42 am

Don't disagree.

But the underlying need is for the RouterOS web server to optionally return a few extra headers for CORS support. This container is only needed to get around that limitation, and kinda inefficient use of system resources. The underlying idea to support "single page webapps" that require only client-side JavaScript (and thus the "app" can be hosted on a static web site, which is cheap/easier than hosting a Python/PHP/Node app). With CORS, node isn't required to use JavaScript to communicate with RouterOS, making easier to start using the REST API.

Since the RouterOS web server is supported all platforms, CORS would be too. Avoids any discussion of the novel use case for containers on TILE. :)
 
xsmael
just joined
Posts: 10
Joined: Wed May 15, 2019 6:33 pm

Re: "Mginx" Container - Reverse Proxy for CORS & X.509 support using Nginx

Tue Jan 03, 2023 11:47 pm

Don't disagree.

But the underlying need is for the RouterOS web server to optionally return a few extra headers for CORS support. This container is only needed to get around that limitation, and kinda inefficient use of system resources. The underlying idea to support "single page webapps" that require only client-side JavaScript (and thus the "app" can be hosted on a static web site, which is cheap/easier than hosting a Python/PHP/Node app). With CORS, node isn't required to use JavaScript to communicate with RouterOS, making easier to start using the REST API.

Since the RouterOS web server is supported all platforms, CORS would be too. Avoids any discussion of the novel use case for containers on TILE. :)
Exaclty what we need! If there was any "feature request" possibility on mikrotik i'd definitely ask this.
 
User avatar
Amm0
Forum Guru
Forum Guru
Topic Author
Posts: 3253
Joined: Sun May 01, 2016 7:12 pm
Location: California

Re: "Mginx" Container - Reverse Proxy for CORS & X.509 support using Nginx

Wed Jan 04, 2023 1:12 am

Exaclty what we need! If there was any "feature request" possibility on mikrotik i'd definitely ask this.
I put tickets in the 'feature request' system at help.mikrotik.com already ;) (#SUP-99112 for CORS, #SUP-99121 for X.509 client+REST). While a container isn't ideal, we use RB1100 a lot, so this approach works well enough for me.

The client-side X.509 support in this container should work – that part is more complex to setup, but likely worth figuring out. Since the X509 certs are pass-thru from OS's keystore/keychain, makes browser+CORS pretty safe and streamlined. Only issue with the container for this... is that it's needs to know a username/password(s) to proxy after X509 auth in nginx, which isn't ideal.
 
xsmael
just joined
Posts: 10
Joined: Wed May 15, 2019 6:33 pm

Re: "Mginx" Container - Reverse Proxy for CORS & X.509 support using Nginx

Wed Jan 04, 2023 10:47 pm

Exaclty what we need! If there was any "feature request" possibility on mikrotik i'd definitely ask this.
I put tickets in the 'feature request' system at help.mikrotik.com already ;) (#SUP-99112 for CORS, #SUP-99121 for X.509 client+REST). While a container isn't ideal, we use RB1100 a lot, so this approach works well enough for me.
That's great, but i went to and couldn't locate the 'feature request' system. however i found a feature request topic on the forum (viewtopic.php?t=45934&start=1500) where you also mention the confusion when deciding where exactly a request should be made. I thought to myself on the new system there should be votes, and i got to vote for this feature :)

The client-side X.509 support in this container should work – that part is more complex to setup, but likely worth figuring out. Since the X509 certs are pass-thru from OS's keystore/keychain, makes browser+CORS pretty safe and streamlined. Only issue with the container for this... is that it's needs to know a username/password(s) to proxy after X509 auth in nginx, which isn't ideal.
Excuse me, I did't understand, this part properly. Are you suggesting me an laternative? or give other details about your solution?
 
User avatar
Amm0
Forum Guru
Forum Guru
Topic Author
Posts: 3253
Joined: Sun May 01, 2016 7:12 pm
Location: California

Re: "Mginx" Container - Reverse Proxy for CORS & X.509 support using Nginx

Thu Jan 05, 2023 1:37 am

Not sure +1's help that much. If it were volume of requests, I'd imagine they would have add mDNS long ago. But they ACK'ed so imagine it's on a list...when, dunno.

X509 is NOT need for CORS. Just their security is consideration in how you'd use CORS, but you kinda need CORS working before moving on X.509.

Basically issue with CORS is the client-side code needs to store the username/password SOMEPLACE. That could be memory (e.g. HTML form field that's not submitted with username/password) or browser's persisted "local storage" can work, both work without X.509. Other approaches to but REST password has to live someplace. But using an X.509 CLIENT-SIDE certificate be better way to authenticate with Mikrotik REST to AVOID needing a password. The build.sh etc here can do this using NGINX's X.509 support, just complex.... You'd can read the commends in the build.sh etc. but it is complex.

But I'd recommend getting the basics working first, before messing with X.509.
 
giguard
newbie
Posts: 35
Joined: Mon Oct 01, 2018 7:10 pm

Re: "Mginx" Container - Reverse Proxy for CORS & X.509 support using Nginx

Mon Feb 12, 2024 9:27 am

First I would like to thank Amm0 for your kind contribution.
I didn't make the proxy running inside the Mikrotik unit. However, I still used nginx and proxy_pass stuff exactly the same.
One problem I came across was when sending PUT request to add in new firewall address-list.
I got an error complaining CORS.
Mind you, GET had no problem. I haven't played with anything else, so no idea if DELETE, PATCH or whatever worked or not.
May be this is a thing with firewall address-list? I don't know. My only need was for firewall address-list so ... anyway.

What I did to get around it was, add another line of add_header 'Access-Control-Allow-Methods' 'GET, POST, PATCH, PUT, DELETE, OPTIONS' always; inside the if ($request_method = 'OPTIONS') if code block.
So the final would look like below,
                if ($request_method = 'OPTIONS') {
                        add_header 'Access-Control-Allow-Origin' '*' always;
                        add_header 'Access-Control-Allow-Methods' 'GET, POST, PATCH, PUT, DELETE, OPTIONS' always;
                        add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
                        add_header 'Access-Control-Max-Age' 1728000;
                        add_header 'Content-Type' 'text/plain; charset=utf-8';
                        add_header 'Content-Length' 0;
                        return 204;
                }
I'm not sure why that is needed but putting there fixed it at least.
Just thought I'd let you know incase you might want to tackle this problem differently.
 
User avatar
Amm0
Forum Guru
Forum Guru
Topic Author
Posts: 3253
Joined: Sun May 01, 2016 7:12 pm
Location: California

Re: "Mginx" Container - Reverse Proxy for CORS & X.509 support using Nginx

Mon Feb 12, 2024 2:56 pm

The docs for NGNIX suggest "If is evil":
https://www.nginx.com/resources/wiki/st ... /ifisevil/

So yeah I'd believe your changes might be right since the "if" does not work like it appears...

One note, the config file here uses environment vars — that's actually a feature specific in the ngnix container, not in a "normal" ngnix package on Linux.

And the X.509 client certificates support add a lot of unnecessary config if you're not using it, too. Now X.509 should work & allows you to hide the router's password behind a certificate — but it's complex.
 
User avatar
Amm0
Forum Guru
Forum Guru
Topic Author
Posts: 3253
Joined: Sun May 01, 2016 7:12 pm
Location: California

Re: "Mginx" Container - Reverse Proxy for CORS & X.509 support using Nginx

Mon Feb 12, 2024 3:13 pm

I didn't make the proxy running inside the Mikrotik unit. However, I still used nginx and proxy_pass stuff exactly the same.
NGNIX works better OUTSIDE a container.

But for containers on Mikroitk, either Traefik or Caddyserver from dockerhub likely be easier – I just knew NGNIX when I wrote this article. But later did get CORS working in a container using a Traefik container: viewtopic.php?t=195259&hilit=traefix#p1026407
Caddyserver is may be easier than Traefik, but I don't have an example.

If NGNIX is working, stick with that. More noting Caddyserver and/or Traefik might be better if running on Mikrotik itself since NGNIX config is NOT very container-friendly. And both Traefik and caddyserver are smaller images.
 
User avatar
Amm0
Forum Guru
Forum Guru
Topic Author
Posts: 3253
Joined: Sun May 01, 2016 7:12 pm
Location: California

Re: "Mginx" Container - Reverse Proxy for CORS & X.509 support using Nginx

Mon Feb 12, 2024 3:21 pm

But would prefer that CORS header be returned by RouterOS. I did file a "feature request" at help.mikrotik.com (SUP-99112) about built-in CORS support for REST.

It really is just half-dozen headers (with very specific things) that REST API need to return & all this nonsense be avoided.

Who is online

Users browsing this forum: No registered users and 21 guests