October 12, 2022

#Troubleshoot CORS related issues on API requests

Cross Origin Resource Sharing (a.k.a. CORS) is a powerful, and yet misunderstood, web standard for protecting web APIs from abuse. If you’re anything like me, however, you had your fair share of wasted work hours trying to deal with it from time to time. The concept seems deceptively simple, but the devil is in the details.

First of all, its important to understand that CORS compliance is an optional standard and is mostly enforced by browsers, like Chrome, Firefox and Safari. Therefore, it’s unlikely that you will face issues with it while working with libraries like Python’s Requests or CLI tools like curl.

However, if you have a frontend application running on your browser with a domain like, let’s say, http://localhost:3000, API calls directed to different domains, like https://laury.dev, will be preceded by a preflight request. This determines if the browser will execute the main request or not.

When having problems with this process, you may see an error similar to this on your Chrome console:

Access to XMLHttpRequest at ‘https://laury.dev from origin ‘http://localhost:3000 has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

To reproduce the problem in a more isolated manner, you can use curl to make a preflight request manually. The important parts here are:

  • the HTTP method has to be OPTIONS
  • three headers must be present: Access-Control-Request-Method, Access-Control-Request-Headers and Origin. Be wary, however, that the domain stated on Origin must mention protocol, FQDN, port (if any is being used), and no trailing slash.

Here’s an example:

curl -IX OPTIONS https://google.com \
    -H "Access-Control-Request-Method: POST" \
    -H "Access-Control-Request-Headers: origin, x-requested-with" \
    -H "Origin: http://mywebsite.com:3000"

For a preflight request to be considered successful, all of the following must be true:

  • the HTTP status on the response is within 200-299
  • the response contains these headers: Access-Control-Allow-Methods, Access-Control-Max-Age and Access-Control-Allow-Origin. One extra catch here is that this last header cannot present the value * when the main request contains authentication information (like cookies). If it does, the preflight will be considered a failure by the browser. In this case, the header has to mention the origin domain explicitly.

Kubernetes users running Ingress Nginx

To manage CORS headers on Ingress Nginx, you may use these annotations on Ingress objects. Even objects that only manage paths inside domains will work with them, so there’s no need to enable CORS on the entire domain if you don’t feel like it.

Further reading

Light overview of preflight requests

Official specification of the Fetch standard for preflight requests

Section break
July 03, 2022

#Use iptables to allow only Cloudflare HTTP requests to target your server

If you’re using Cloudflare to protect HTTP endpoints, it might be worth it to block any web traffic that doesn’t come from their servers.

To do that, let’s use iptables. The following commands will do the trick, but take note that these IPs might change in the future.

# Allow IPv4 traffic
iptables -I INPUT -p tcp -m multiport --dports http,https -s -j ACCEPT
iptables -I INPUT -p tcp -m multiport --dports http,https -s -j ACCEPT
iptables -I INPUT -p tcp -m multiport --dports http,https -s -j ACCEPT
iptables -I INPUT -p tcp -m multiport --dports http,https -s -j ACCEPT
iptables -I INPUT -p tcp -m multiport --dports http,https -s -j ACCEPT
iptables -I INPUT -p tcp -m multiport --dports http,https -s -j ACCEPT
iptables -I INPUT -p tcp -m multiport --dports http,https -s -j ACCEPT
iptables -I INPUT -p tcp -m multiport --dports http,https -s -j ACCEPT
iptables -I INPUT -p tcp -m multiport --dports http,https -s -j ACCEPT
iptables -I INPUT -p tcp -m multiport --dports http,https -s -j ACCEPT
iptables -I INPUT -p tcp -m multiport --dports http,https -s -j ACCEPT
iptables -I INPUT -p tcp -m multiport --dports http,https -s -j ACCEPT
iptables -I INPUT -p tcp -m multiport --dports http,https -s -j ACCEPT
iptables -I INPUT -p tcp -m multiport --dports http,https -s -j ACCEPT
iptables -I INPUT -p tcp -m multiport --dports http,https -s -j ACCEPT

# Allow IPv6 traffic
ip6tables -I INPUT -p tcp -m multiport --dports http,https -s 2400:cb00::/32 -j ACCEPT
ip6tables -I INPUT -p tcp -m multiport --dports http,https -s 2606:4700::/32 -j ACCEPT
ip6tables -I INPUT -p tcp -m multiport --dports http,https -s 2803:f800::/32 -j ACCEPT
ip6tables -I INPUT -p tcp -m multiport --dports http,https -s 2405:b500::/32 -j ACCEPT
ip6tables -I INPUT -p tcp -m multiport --dports http,https -s 2405:8100::/32 -j ACCEPT
ip6tables -I INPUT -p tcp -m multiport --dports http,https -s 2a06:98c0::/29 -j ACCEPT
ip6tables -I INPUT -p tcp -m multiport --dports http,https -s 2c0f:f248::/32 -j ACCEPT

Now, block everything else on ports 80 and 443.

iptables -A INPUT -p tcp -m multiport --dports http,https -j DROP
ip6tables -A INPUT -p tcp -m multiport --dports http,https -j DROP

Test your new rules and save your changes to persist them across reboots.

netfilter-persistent save

Further reading

Cloudflare’s IP list

Official Cloudflare recommendations to protect your origin servers

Section break
July 02, 2022

#Basic survival guide and commands for iptables

iptables is a Linux firewall tool that manages packet routing and can block or allow traffic based on rules like a packet’s origin or destination.

Its settings are organized in tables which contain sets of rules, called chains, that will filter data packets.

# Check current configuration
iptables -L -v

# Accept all requests from an IP
iptables -A INPUT -s <ip> -j ACCEPT

# Drop all requests from an IP
iptables -A INPUT -s <ip> -j DROP

# Drop all requests from a range of IPs
iptables -A INPUT -m iprange --src-range <ip_range_start>-<ip_range_end> -j DROP

# Drop all traffic (must be run after acceptance rules)
iptables -A INPUT -j DROP

# Delete all current rules
iptables -F

# Persist changes to disk
# Depending on your setup, you may need to run another command instead
netfilter-persistent save

Section break
June 26, 2022

#Protect public URLs with ModSecurity for Nginx on a Kubernetes cluster

If you’re using Ingress Nginx to manage public endpoints, ModSecurity is already installed, but it’s disabled by default. To enable it, add the following to the ConfigMap ingress-nginx-controller:

allow-snippet-annotations: "true"
enable-modsecurity: "true"
enable-owasp-modsecurity-crs: "true"
modsecurity-snippet: |-
  SecRuleEngine On
  SecRequestBodyAccess On

Customize ModSecurity rules on a per host basis

It’s possible to adjust the ModSecurity configuration for each Ingress object on the cluster using annotations.

  nginx.ingress.kubernetes.io/enable-modsecurity: "true"
  nginx.ingress.kubernetes.io/enable-owasp-core-rules: "false"
  nginx.ingress.kubernetes.io/modsecurity-snippet: |
    SecRuleRemoveById <rule_id>

How to completely customize modsecurity.conf

With the modsecurity-snippet option, its possible to add custom configuration to ModSecurity. However, you can override the modsecurity.conf entirely if you want.

To do so, first copy the file inside the pod.

kubectl -n ingress-nginx cp <ingress-controller-pod-name>:/etc/nginx/modsecurity/modsecurity.conf ./modsecurity.conf

Modify it as required and save the file in a ConfigMap.

kubectl -n ingress-nginx create configmap modsecurityconf --from-file=modsecurity.conf

If, after creating the initial version, you need to change the file again locally, you can update the ConfigMap by doing the following.

kubectl -n ingress-nginx create configmap modsecurityconf \
  --from-file=modsecurity.conf -o yaml \
  --dry-run=client | kubectl apply -f -

To mount the ConfigMap to the controller deployment, create a patch file.

      - name: modsecurityconf
          name: modsecurityconf
      - name: controller
        - name: modsecurityconf
          mountPath: "/etc/nginx/modsecurity/modsecurity.conf"
          subPath: modsecurity.conf
          readOnly: true

And then apply the patch file.

kubectl -n ingress-nginx patch deployment/ingress-nginx-controller --patch-file deployment-patch.yml

Further reading

ConfigMap options for Ingress Nginx

Annotations options for Ingress objects

Section break
June 24, 2022

#Host publicly accessible web URLs from a home server using a remote proxy

Ideally, you would be able to open ports 80 and 443 of your public IP address and update DNS entries with solutions like ddclient or DNS-O-Matic.

However, if your ISP blocks direct traffic to those ports, its possible to use a remote server and SSH tunneling to bypass that limitation. With this setup, you can use Kubernetes ingresses normally. From the ingress’ perspective, http traffic will reach your home server directly. Therefore, tooling like Nginx and Cert Manager will work with no special tweaks.

Any Linux box with a public IP can be your remote proxy, but Oracle Cloud has a generous free tier and can be a good starting option. Just remember that the smallest available machine is probably enough for your needs since it will only forward traffic and not really act on it.

After setting up the remote server with your provider of choice, ensure that it has a SSH server running. We’re going to use it to forward network packets for us.

On your remote instance, edit the /etc/ssh/sshd_config file and add GatewayPorts yes to it. Apply the new configuration with service sshd restart.

If you opted for an Oracle Cloud server, go to the Ingress Rules section of the Virtual Cloud Network panel and open ports 80 and 443. Also, adjust iptables in the instance to allow public traffic to ports 80 and 443.

iptables -I INPUT 6 -m state --state NEW -p tcp --dport 80 -j ACCEPT
iptables -I INPUT 6 -m state --state NEW -p tcp --dport 443 -j ACCEPT
netfilter-persistent save

From your home server, you can run a quick test, as long as you already have a HTTP server running locally. Don’t forget to generate a new SSH key for your local server and authorize it on the remote machine. If everything until now went smoothly, you will be able to access a local web endpoint using the remote server IP as the DNS entry for your domain.

ssh -N -R 80:<local_ip>:80 root@<remote_ip>

If the port forwarding worked, you can create deployment files for it so Kubernetes itself can maintain the connection up and running for you. Customize and apply the following to your local cluster.

apiVersion: v1
kind: Namespace
  name: proxy-router

apiVersion: apps/v1
kind: Deployment
  name: autossh-80
  namespace: proxy-router
    app: autossh-80
  replicas: 1
      maxSurge: 0
      maxUnavailable: 1
    type: RollingUpdate
      app: autossh-80
        app: autossh-80
      - name: autossh-80
        image: jnovack/autossh:2.0.1
          - name: SSH_REMOTE_USER
            value: "root"
          - name: SSH_REMOTE_HOST
            value: "<remote_server_ip>"
          - name: SSH_REMOTE_PORT
            value: "22"
          - name: SSH_TUNNEL_PORT
            value: "80"
          - name: SSH_BIND_IP
            value: ""
          - name: SSH_TARGET_HOST
            value: "<local_server_ip>"
          - name: SSH_TARGET_PORT
            value: "80"
          - name: SSH_MODE
            value: "-R"
          - name: keys
            mountPath: /id_rsa
      nodeName: <node_with_ingress_enabled>
      hostNetwork: true
        - name: keys
            path: <node_path_for_ssh_keys>
            type: File

To redirect port 443 as well, create another deployment using the previous one as a reference.

Any public URLs should, of course, have their DNS entries pointing to the remote server’s IP.

Section break
August 20, 2020

#Issue Let's Encrypt certificates for domains in a Kubernetes cluster

We will use cert-manager to issue HTTPS certificates for domains served publicly by a Kubernetes cluster.

The application can be installed via Helm, but it’s recommended to install CRDs (Custom Resource Definitions) separately. That may come in handy if you need to delete cert-manager without losing already issued certificates. Don’t forget to change the “version” accordingly on all commands.

kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v0.16.1/cert-manager.crds.yaml

Add JetStack charts to your Helm client.

helm repo add jetstack https://charts.jetstack.io
helm repo update

Install the application.

helm install \
  cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --version v0.16.1

To issue actual certificates, you will need a production Issuer or ClusterIssuer. The following yaml defines a ClusterIssuer and assumes that you have a nginx ingress controller. If it suit your needs, apply it to your cluster.

apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
  name: letsencrypt-prod
    # The ACME server URL
    server: https://acme-v02.api.letsencrypt.org/directory
    # Email address used for ACME registration
    email: <your_email>
    # Name of a secret used to store the ACME account private key
      name: letsencrypt-prod
    # Enable the HTTP-01 challenge provider
    - http01:
          class: nginx

Verify if your set up is working.

$ kubectl get clusterissuers -A
NAME               READY   AGE
letsencrypt-prod   True    30m

The cert-manager stack will act upon ingresses that comply with a couple of conditions to generate HTTPS certificates. The following example illustrates that. Take special note of the annotations and the “secretName” property under “tls”.

apiVersion: extensions/v1beta1
kind: Ingress
    cert-manager.io/cluster-issuer: letsencrypt-prod
    kubernetes.io/tls-acme: "true"
  name: nginx-ingress
  namespace: ingress-testing
  - host: <public_URL>
      - backend:
          serviceName: <service_name>
          servicePort: 80
  - hosts:
    - <public_URL>
    secretName: <secret_name_of_your_choosing>

Rancher users

If you’re installing the Helm Chart via Rancher, you may see the error release cert-manager failed: resource's namespace kube-system doesn't match the current namespace cert-manager. This happens because cert-manager tries to make changes to more than one namespace and Rancher doesn’t support that. There is an issue discussing this.

Thankfully, cert-manager offers an option to only operate in one namespace. Set “global.leaderElection.namespace” to “cert-manager” to achieve this.

helm install \
  cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --version v0.16.1 \
  --set global.leaderElection.namespace=cert-manager

k3s users

In k3s, at least up to version 1.18.x, cert-manager has problems running its webhook. At this time, to deal with this, we have to use an older version of cert-manager that can run without it. Set “webhook.enabled” to “false” and use v0.13.1.

helm install \
  cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --version v0.13.1 \
  --set webhook.enabled=false

Further reading

Official documentation on the installation process

Section break
August 11, 2020

#Pretty-print JSON files using the command line

If you have either Python 2 or 3 installed, use the “json.tool” module to pretty-print.

python -m json.tool <json_file> | less

You can also stream input to Python instead of passing a file directly.

cat <json_file> | python -m json.tool | less

This may be useful for viewing large minified JSON files that could slow down GUI applications.

Section break
August 03, 2020

#Quick usage examples of "find" on the command line

Search recursively for file names using a regex pattern. Use -name for a case sensitive search.

find . -iname '*<partial_name>*'

To exclude results located in certain paths, use -not -path:

find . -iname '*<partial_name>*' -not -path "*/.venv/*" 

Execute a command with every result found.

find . -iname '*.md' -type f -exec ls -lh {} \;

List all files in the current folder (recursively) that contain every string mentioned in the search parameters. You can chain as many search terms as you want. Just remember that the last grep needs to have -l as a parameter instead of -q.

find . -type f -exec grep -q '<string1>' {} \; -exec grep -q '<string2>' {} \; -exec grep -l '<string3>' {} \;

Note: every command needs to be terminated by ; or +. But these signs may need to be escaped as in ; or \;. Outputs given by find will be placed on {}. Multiple uses of -exec are allowed.

Section break
August 02, 2020

#Read Kubernetes API data using Golang

Get the API client.

import (
  metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

// use the current context in kubeconfig
config, err := clientcmd.BuildConfigFromFlags("", "<path_to_KUBECONFIG>")
if err != nil {

// create the clientset
clientset, err := kubernetes.NewForConfig(config)
if err != nil {

Usage examples:

  • read data about servers
nodeList, err := clientset.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{})
if err != nil {
  • read data about pods
namespace := "example"  // leave empty to get data from all namespaces
podList, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{})
if err != nil {

In the above examples, ListOptions can take two important strings, field selectors and label selectors. This can be used for filtering results.

  LabelSelector: "labelName=labelKey",
  FieldSelector: "spec.nodeName=<node_name>",  // Example for filtering by node name

Field selectors and label selectors on this case operate in the same way that the CLI options for kubectl do. So, the same rules apply here.

Further reading

Official docs on Field Selectors and Labels.

Section break
August 02, 2020

#Read Kubernetes metrics-server data using Golang

First, get the metrics API client.

import (
  metricsv "k8s.io/metrics/pkg/client/clientset/versioned"

// Get the config
config, err := clientcmd.BuildConfigFromFlags("", "<path_to_KUBECONFIG>")
if err != nil {

// Get the metrics client
metricsClientset, err := metricsv.NewForConfig(config)
if err != nil {

Get the data and store it in podMetricsList array.

namespace := "example"  // leave empty to get data from all namespaces
podMetricsList, err := metricsClientset.MetricsV1beta1().PodMetricses(namespace).List(context.TODO(), metav1.ListOptions{})
if err != nil {

Iterate over the results found.

for _, v := range podMetricsList.Items {
  fmt.Printf("%s\n", v.GetName())
  fmt.Printf("%s\n", v.GetNamespace())
  fmt.Printf("%vm\n", v.Containers[0].Usage.Cpu().MilliValue())
  fmt.Printf("%vMi\n", v.Containers[0].Usage.Memory().Value()/(1024*1024))

Note: if you’re inspecting pods that may have more than one container, you’ll need to iterate over v.Containers as well.

Section break
August 01, 2020

#Receive data from Kubernetes API using curl

Start by running kubectl’s proxy. With its help, we avoid dealing with authentication headers.

kubectl proxy

Test the connection with a basic call.

curl http://localhost:8001/api/

To check the API groups available, access the root.

curl http://localhost:8001/apis/

Based on the group listing, you can infer URLs for resources.

For cluster-scoped resources, use:


And, for namespace-scoped resources:


To get deployment data, for example, the API Group is “apps”, Version is “v1” and Resourcetype is “deployments”. The final request looks like this:

curl http://localhost:8001/apis/apps/v1/deployments

If your Kubernetes cluster has a metrics-server, the following request will get node resource usage data:

curl http://localhost:8001/apis/metrics.k8s.io/v1beta1/nodes

Rancher users

However, if your cluster was created using Rancher, you’ll need “Authorized Cluster Endpoint” enabled, so your requests can reach the API Server directly. Your URL paths will slightly differ from the examples above otherwise.

Using your command line, you’ll also need to select the correct context for kubectl before activating the proxy.

# List available contexts
kubectl config get-contexts

# Select the direct API context
# Its name has the format <cluster_name>-<pilot_name>
kubectl config use-context <direct_context>

Further reading

Overview of the Kubernetes API

More info about API concepts

Accessing the API without kubectl proxy

Full list of Groups, Versions and Resourcetypes

Section break
July 31, 2020

#Activate the replicaset mode in MongoDB

Start the MongoDB daemon with the --replSet flag.

mongod --replSet rs0

After first start, initialize the replica set with the following:

mongo <mongo_host>/<database> --eval "rs.initiate({_id: 'rs0', members: [ { _id: 0, host: '<mongo_host>:27017' } ]})"

Section break
Section break
July 23, 2020

#Test networking performance between two Linux machines

Let’s use iperf to measure networking performance. First, set a “server” machine.

iperf -s

From a “client” machine, run the test.

iperf -c <server_ip>

If you are having trouble getting the server’s IP, use ip addr show to find addresses used by the current machine in any connected networks.

Section break
July 23, 2020

#Wipe any filesystem data and partitions from devices on Linux

wipefs should work in most cases to clean filesystems.

# View filesystem data from device
wipefs /dev/<my_device>

# Wipe any filesystem found
wipefs --all /dev/<my_device>

If wipefs can’t detect a filesystem, you can quickly wipe the header data of the device with dd.

dd if=/dev/zero of=/dev/<my_device> bs=1M count=100 oflag=direct,dsync status=progress

You may need to restart your machine to make the filesystem deletion known to the OS, if your device is a “LVM2_member” for instance.

Also, as a last resort, you can completely wipe the device.

dd if=/dev/zero of=/dev/<my_device> bs=1M count=5000000 oflag=direct,dsync status=progress

To delete partitions, sgdisk should work well.

sgdisk --zap-all /dev/<my_device>

Section break
July 16, 2020

#Download files recursively through a FTP connection using wget

Unfortunately, SCP and Rsync aren’t always an option to get our files out of a server. When FTP is all we have, wget can still do a pretty good job and not re-download files you already have, like a primitive Rsync.

wget -nc -nH --cut-dirs=<NUMBER_REMOTE_FOLDERS_TO_CUT_FROM_PATH> --ftp-user='<FTP_USER>' --ftp-password='<FTP_PASSWORD>' -rl 0 -P <LOCAL_PATH> 'ftp://<REMOTE_HOST>/<PATH_TO_FILES>'

Explanation for the used flags:

-nc, --no-clobber                skip downloads that would download to existing files (overwriting them)
-nH, --no-host-directories       don't create host directories
--cut-dirs=NUMBER                ignore NUMBER remote directory components
-r,  --recursive                 specify recursive download
-l,  --level=NUMBER              maximum recursion depth (inf or 0 for infinite)
-P,  --directory-prefix=PREFIX   save files to PREFIX/..

Section break
June 26, 2020

#Check a site's downtime with this bash one-liner

During a site’s downtime, the following line will print the current time and try to access the given URL one time every 2 seconds. Only the received HTTP headers will be shown.

You may find it useful for monitoring downtime while you try to solve a problem.

while true; do date; curl -IX GET '<URL>'; sleep 2; done;

Section break
Section break
March 10, 2020

#How to dump and restore PostgreSQL databases

Options for dumping data:

# Binary mode
pg_dump -Fc -U $POSTGRES_USER $POSTGRES_DB > dump-`date '+%Y_%m_%d__%H_%M_%S'`.psqlc

# SQL mode
pg_dump -U $POSTGRES_USER $POSTGRES_DB > dump-`date '+%Y_%m_%d__%H_%M_%S'`.sql

# Docker and SQL mode
docker exec -it -u postgres <container_name> sh -c 'pg_dump -U $POSTGRES_USER $POSTGRES_DB' > dump-`date '+%Y_%m_%d__%H_%M_%S'`.sql

# Kubernetes and SQL mode (vulnerable to networking failures)
kubectl -n <namespace> exec deploy/postgres -- sh -c 'pg_dump -U $POSTGRES_USER $POSTGRES_DB' > dump-`date '+%Y_%m_%d__%H_%M_%S'`.sql

Options for restoring data:

# Binary mode
pg_restore -O -U $POSTGRES_USER -c -x -n public -d $POSTGRES_DB dump.psqlc

# SQL mode
psql -d $POSTGRES_DB -a -f /backups/dump.sql

If you’re using Docker, you can also place dump files on /docker-entrypoint-initdb.d and those will be imported on the first run. Accepted files types are *.sql, *.sql.gz, and *.sh.

Section break
Section break
Section break
March 02, 2020

#Format a new SSD/HD drive for usage

# Create the partition
parted --align optimal <raw_device>
mklabel msdos
mkpart primary ext4 0% 100%

# Get the new partition device path

# Make the filesystem
mkfs.ext4 <new_partition_device_path>

# Test the partition
mkdir /tmp/partition-test
mount -t ext4 <new_partition_device_path> /tmp/partition-test

# Get the UUID

# Edit the /etc/fstab to mount partition during boot
UUID=<uuid>     /storage        ext4    defaults,discard        0       2
## OR, if using XFS
UUID=<uuid>     /storage       xfs     defaults        1       2

Section break
Section break
July 01, 2019

#Dump and restore data from MongoDB in Docker

# Create the database dump on your server
docker exec <my_mongodb_container> mongodump --archive=/backups/mongodb-`date +%Y%m%d`.gz --gzip --db <database_name>

# Copy it to your local machine, if needed
scp -r <server_user>@<server_ip>:<remote_backup_path> <local_backup_path>

# Restore it directly into a running Docker container
zcat <backup_path> | docker exec -i <my_mongodb_container> mongorestore --archive --drop

Section break
Section break