Nginx-Ingress Reverse Proxy in Front of an Object Storage for inflexible WebGL and stubborn CORS

Rationale: in an Unity App generating WebGL with remote-assets loading where we have very little control on the generated code, I’m limited to one solution and it’s to comply with CORS: hosting assets in the same domain as the app is running in.
In this context it seems indeed impossible to set correctly the crossorigin attribute in the generate WebGL code. If you think otherwise please teach me in the comments.

TLDR; It works fine of course, but don’t waste your time deploying your own reverse proxy if you want to keep the benefit of a CDN and if performances is a key in your deployment.

This wouldn’t be an issue if the current provider, DigitalOcean, would allowing me to add custom hostname to its Object Storage offer (Spaces). It actually allows it but only for domain managed by DigitalOcean itself. Which is nonsense to anyone able to add a DNS entry (anyone with two fingers and a tong?) and no-go for us as our DNS is hosted by CloudFlare and acts as a good protection for the infrastructure.

CloudFlare is a reverse proxy (and more) and hide our origin servers from the wild wild web. I would have expect this provider to allow me to proxy our origin server to any FQDN. For instance front.org.tld to forward requests and return content from cdn.location.digitaloceanspaces.com. This is unfortunately not available for the free tier and it seems available to the business plan under the name CNAME Setup, but I don’t agree with the price for such a “simple” feature. We are already paying with our data and it seems that concurrent are offering this feature to free tiers.

I excluded the idea to “write” a reverse proxy in Javascript and use CloudFlare workers because… well it’s nonsense shitty tech. The net is not a trash and needs no more bullshit solutions.

Our services are running in a Kubernetes cluster so if I tolerate the performance trade off to run a reverse proxy in my own rented infrastructure, this solution is “free”, relatively clean and allow us to get forward with our project.

There is drawbacks. Many. For anticipated heads-up and warnings, see the end of this post.

With the bellow service and ingress settings, requests sent to OBJECT-STORAGE-RP.ORG-DOMAIN.TLD (any FQDN) will return content from OBJECT-STORAGE-HOSTNAME (Object Storage on DigitalOcean Spaces, Amazon S3, Google Cloud Storage, etc.).

---
apiVersion: v1
kind: Service
metadata:TODO
  namespace: APP-NAMESPACES
  name: OBJECT-STORAGE-RP
  labels:
    app: OBJECT-STORAGE-RP
spec:
  externalName: OBJECT-STORAGE-HOSTNAME
  type: ExternalName
  selector:
    app: OBJECT-STORAGE-RP

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  namespace: APP-NAMESPACES
  annotations:
    kubernetes.io/ingress.class: "nginx"
    cert-manager.io/cluster-issuer: ORG-SSL-ISSUER
    nginx.ingress.kubernetes.io/backend-protocol: https
    nginx.ingress.kubernetes.io/upstream-vhost: "OBJECT-STORAGE-HOSTNAME"
    nginx.ingress.kubernetes.io/server-snippet: |
      proxy_ssl_name OBJECT-STORAGE-HOSTNAME;
      proxy_ssl_server_name on;
  name: OBJECT-STORAGE-RP-ingress
  labels:
    app: OBJECT-STORAGE-RP

spec:
  tls:
  - hosts:
    - OBJECT-STORAGE-RP.ORG-DOMAIN.TLD
    secretName: OBJECT-STORAGE-RP-ORG-TLS
  rules:
  - host: OBJECT-STORAGE-RP.ORG-DOMAIN.TLD
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: OBJECT-STORAGE-RP
            port:
              number: 443

Where variables are:

  • OBJECT-STORAGE-RP = a name to define this reverse proxy
  • OBJECT-STORAGE-HOSTNAME = source host, where assets are hosted (Spaces in my case)
  • ORG-SSL-ISSUER = when using SSL, might be letsencrypt (certbot) and this is cluster-dependent
  • APP-NAMESPACES = a meaningful or random namespace
  • OBJECT-STORAGE-RP.ORG-DOMAIN.TLD = the reverse proxy FQDN, might completely differ from OBJECT-STORAGE-RP, or not

On one hand there is notable performances trade offs: most object storage provider offer a CDN option to boost assets access in client’s browsers and this feature won’t be a viable option.
If you use signed access it won’t be possible to add a caching CDN in front of this reverse proxy. This instance can still be hidden by CloudFlare for instance, but with no performances improvement as the caching won’t work.

Doing so I’m wasting resources because my queries are going through many gateways (proxies) and encrypt/decrypt queries for nothing more than renaming. And I’m paying (a bit) for it.

On the other hand there is advantages. Doing so is also blurring lines, which is a security improvement (and even a business if you look at CloudFront and other Cloud-Behemoth).

It’s working, settings are flexible and this means that we can continue to work without CORS issues.

However this is not a solution in itself. The right way would be either of the following:

  • being able to setup our own hostname (FQDN) directly for the bucket of our Object Storage provider.
  • setting up the Cloud reverse proxy directly to point to the right origin server. CloudFlare in our case.

Resources:

Remove VPN/Network from Unify Controller using Command Line

After migrating a site from a self hosted network controller to a new Unify Cloud Key, I found myself in an annoying position: not being able to remove an old VTI VPN (from the previous configuration). The UI just didn’t offer this option like it should and actually does for other networks and VPNs. Searching the wild wild web didn’t help either, so I had to be creative.

But first let’s roll back a bit in time to better explain the issue: right after importing the site configuration, I had two sites configured. The “default” and “SITE2” sites. My newly imported “SITE2” site wasn’t the default one and this was an issue. I had to change it manually using this CLI technique because the UI doesn’t allow it.

So, based on the above mentioned technique, I succeeded to remove an old network from the settings, where the UI wasn’t competent.

SSH to your Cloud Key/Docker/Server wherever the Unify Network Controller is hosted. Then start the mongo DB CLI with mongo --port 27117.

Switch to the Network Controller database with use ace then get the list of networks with db.networkconf.find(). You should get something like this:

{ "_id" : ObjectId("HEX1"), "attr_no_delete" : true, "attr_hidden_id" : "WAN", "wan_networkgroup" : "WAN", "site_id" : "345634563465", "purpose" : "wan", "name" : "Default (WAN1)", "wan_type" : "pppoe", "wan_ip" : "80.153.183.45", "wan_username" : "LOGIN", "wan_type_v6" : "disabled", "x_wan_password" : "MAYBEMAYBE", "wan_provider_capabilities" : { "download_kilobits_per_second" : 250000, "upload_kilobits_per_second" : 40000 }, "report_wan_event" : false, "wan_load_balance_type" : "failover-only", "wan_load_balance_weight" : 50, "wan_vlan_enabled" : false, "wan_vlan" : "", "wan_egress_qos" : "", "wan_smartq_enabled" : true, "mac_override_enabled" : false, "wan_dhcp_options" : [ ], "wan_ip_aliases" : [ ], "wan_dns_preference" : "auto", "setting_preference" : "manual", "wan_smartq_up_rate" : 40000, "wan_smartq_down_rate" : 250000 }
{ "_id" : ObjectId("HEX2"), "purpose" : "guest", "networkgroup" : "LAN", "dhcpd_enabled" : true, "dhcpd_leasetime" : 86400, "dhcpd_dns_enabled" : false, "dhcpd_gateway_enabled" : false, "dhcpd_time_offset_enabled" : false, "ipv6_interface_type" : "none", "ipv6_pd_start" : "::2", "ipv6_pd_stop" : "::7d1", "gateway_type" : "default", "nat_outbound_ip_addresses" : [ ], "name" : "Guests", "vlan" : "2", "ip_subnet" : "192.168.7.1/24", "dhcpd_start" : "192.168.7.100", "dhcpd_stop" : "192.168.7.254", "dhcpguard_enabled" : true, "dhcpd_ip_1" : "192.168.7.1", "enabled" : true, "is_nat" : true, "dhcp_relay_enabled" : false, "vlan_enabled" : true, "site_id" : "123453", "lte_lan_enabled" : false, "setting_preference" : "manual", "mdns_enabled" : false, "auto_scale_enabled" : false, "upnp_lan_enabled" : false }
{ "_id" : ObjectId("HEX3"), "attr_hidden_id" : "WAN_LTE_FAILOVER", "wan_networkgroup" : "WAN_LTE_FAILOVER", "purpose" : "wan", "name" : "LTE Failover WAN", "site_id" : "3563465", "wan_type" : "static", "report_wan_event" : true, "wan_load_balance_type" : "failover-only", "wan_ip" : "IPADDRESS", "wan_gateway" : "IPADDR", "wan_netmask" : "255.255.255.254", "enabled" : true, "ip_subnet" : "192.168.123.161/30", "wan_dns_preference" : "auto", "setting_preference" : "auto" }
{ "_id" : ObjectId("HEX4"), "enabled" : true, "purpose" : "remote-user-vpn", "ip_subnet" : "192.168.2.1/24", "l2tp_interface" : "wan", "l2tp_local_wan_ip" : "any", "vpn_type" : "l2tp-server", "x_ipsec_pre_shared_key" : "SECRET", "setting_preference" : "auto", "site_id" : "12345, "name" : "VPN Server", "l2tp_allow_weak_ciphers" : false, "require_mschapv2" : false, "dhcpd_dns_enabled" : false, "radiusprofile_id" : "1234" }

Find the network that you cannot remove from the UI and type db.networkconf.deleteOne({ _id: ObjectId("HEX3") }) where HEX3 is your network ID. exit the CLI and check in the UI that the network has indeed been removed.

You default network should be the imported network now.