I recently needed to tackle a problem for one of my users. I employ whitelists for more sensitive services, which include some Dropbox-like functionality. One of my users was unable to use a VPN, but their IP address kept rotating.
How can I update nginx whitelists as well as firewall rules automatically (and maybe somewhat safely)? Read on for my latest crime against Kubernetes.
How is access handled with Synology Drive?
Synology Drive in my environment has two components. The first is the web interface, which is front-ended by a Kubernetes ingress with an associated service and endpoint (because the actual web interface is, of course, on the Synology). The basic configuration looks like this:
kind: Ingress apiVersion: networking.k8s.io/v1 metadata: name: ingress-synology namespace: externalsvc annotations: cert-manager.io/cluster-issuer: letsencrypt-prod-dns01 kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/proxy-body-size: "0" nginx.ingress.kubernetes.io/proxy-read-timeout: "600" nginx.ingress.kubernetes.io/proxy-send-timeout: "600" nginx.ingress.kubernetes.io/whitelist-source-range: "10.0.0.0/8,136.226.0.0/16,67.183.150.241/32,71.212.140.237/32,71.212.91.169/32,73.42.224.105/32,76.22.86.230/32" spec: ...
The important line is the nginx.ingress.kubernetes.io/whitelist-source-range
annotation, which controls who can connect. Without it, users will get a 403 error.
The second piece is a firewall rule to allow access to 6690, which is the TCP port that Synology Drive uses for transmitting data.
Luckily, my firewall is configured by a script that flushes all rules and then configures the firewall in the form of iptables
commands (I am that old). An example of the rule looks like this:
iptables -A FORWARD -s "$SRC" -d "$HOST_NAS01" -p tcp --dport "$PORT" -j ACCEPT
Reading the latest IP address from Tautulli
Because my user sometimes watches videos on Plex, I have a log of some metadata from the stream. This includes the IP address. It turns out Tautulli has a decent API I could use.
curl -s "https://tautulli.ccrow.org/api/v2?apikey=<TAUTULLI_API_KEY>&cmd=get_history&user=<USER>&length=1" \ | jq -r '.response.data.data[0].ip_address // empty'
The above curl command uses the API to run the get_history
command for a given user. This provides a JSON payload. The legth
parameter ensures I only return a single item, which I opted for instead of parsing all of the user’s watch history entries locally. We pipe that into jq to extract the IP address and return nothing if it isn’t found, which is helpful for our bash command.
The goal of our script is to produce a text file that contains one line per CIDR address that we can use to update our firewall rule, as well as the ingress whitelist. We need to do a few things to the result, like deduplicating the CIDR addresses. There seems to be a shell command for everything these days:
# Loop over users and fetch most recent IP for user in "${USERS[@]}"; do ip=$(curl -s "${TAUTULLI_URL}?apikey=${APIKEY}&cmd=get_history&user=${user}&length=1" \ | jq -r '.response.data.data[0].ip_address // empty') if [[ -n "$ip" ]]; then echo "${ip}/32" >> "$CIDR_FILE" else echo "No IP found for $user" >&2 fi done ### Deduplicate and sort sort -u "$CIDR_FILE" -o "$CIDR_FILE" echo "CIDR list built at $CIDR_FILE"
Updating our Ingress whitelist
We can then update a list of ingress YAML definitions to update the whitelist:
# --- Update ingress files --- # Create a comma-separated string of CIDRs CIDR_LIST=$(paste -sd, "$CIDR_FILE") for file in "${INGRESS_FILES[@]}"; do if [[ -f "$file" ]]; then sed -i "s#nginx.ingress.kubernetes.io/whitelist-source-range:.*#nginx.ingress.kubernetes.io/whitelist-source-range: \"$CIDR_LIST\"#" "$file" echo "Updated whitelist-source-range in $file" else echo "File not found: $file" fi done
Why use sed? Some of my YAML files have multiple manifests that are delimited by ---
, and I was not very uniform. Sed is a simple way to find and replace the existing string.
We then simply check our changes into git for ArgoCD to sweep up and apply.
Updating our firewall rules
I mentioned that my firewall is simply a script that is run over SSH. I ensured that our $CIDR_FILE is copied alongside the script and updated my iptables
script to include a simple loop:
### Clients I allow to connect to synology drive CIDR_FILE="cidrs.txt" # --- Ports to allow --- PORTS=(6690 6281) # You can adjust per requirement # --- Apply iptables rules --- if [[ ! -f "$CIDR_FILE" ]]; then echo "CIDR file not found: $CIDR_FILE" exit 1 fi while IFS= read -r SRC; do for PORT in "${PORTS[@]}"; do iptables -A FORWARD -s "$SRC" -d "$HOST_NAS01" -p tcp --dport "$PORT" -j ACCEPT echo "Added rule: $SRC -> $HOST_NAS01 port $PORT" done done < "$CIDR_FILE"
The above will insert two rules per client CIDR to allow TCP port 6690 and 6281. We loop through the file using the SRC
variable as a placeholder for the line, we then loop through the PORTS
array to add the rule. The above implementation will be very different depending on your firewall.
For those that want to see the update script, which is sadly triggered by a cronjob, in all of its glory:
#!/usr/bin/env bash set -euo pipefail ### --- Config --- source /root/secrets.sh APIKEY=$TAUTULLI_API_TOKEN TAUTULLI_URL="https://tautulli.ccrow.org/api/v2" CIDR_FILE="$HOME/personal/homelab/tautulli-api/cidrs.txt" FIREWALL_DIR="$HOME/personal/firewall" # Static CIDRs STATIC_CIDRS=( "136.226.0.0/16" "10.0.0.0/8" ) # Usernames to query USERS=( "User1" "User2" ) # GitOps repo base directory BASEDIR="$HOME/personal/gitops-cd" INGRESS_FILES=( "$BASEDIR/manifests/externalsvc/synology.yaml" "$BASEDIR/manifests/kiwix/kiwix.yaml" ) ### --- Build CIDR file --- # Add static CIDRs for cidr in "${STATIC_CIDRS[@]}"; do echo "$cidr" >> "$CIDR_FILE" done # Loop over users and fetch most recent IP for user in "${USERS[@]}"; do ip=$(curl -s "${TAUTULLI_URL}?apikey=${APIKEY}&cmd=get_history&user=${user}&length=1" \ | jq -r '.response.data.data[0].ip_address // empty') if [[ -n "$ip" ]]; then echo "${ip}/32" >> "$CIDR_FILE" else echo "No IP found for $user" >&2 fi done ### Deduplicate and sort sort -u "$CIDR_FILE" -o "$CIDR_FILE" echo "CIDR list built at $CIDR_FILE" # --- Update ingress files --- # Create a comma-separated string of CIDRs CIDR_LIST=$(paste -sd, "$CIDR_FILE") for file in "${INGRESS_FILES[@]}"; do if [[ -f "$file" ]]; then sed -i "s#nginx.ingress.kubernetes.io/whitelist-source-range:.*#nginx.ingress.kubernetes.io/whitelist-source-range: \"$CIDR_LIST\"#" "$file" echo "Updated whitelist-source-range in $file" else echo "File not found: $file" fi done echo "updating git in 10 sec" sleep 10 # --- Git commit changes --- cd "$BASEDIR" #git pull || echo "ERROR: can't pull"; exit 1 if [[ -n "$(git status --porcelain)" ]]; then git add . git commit -m "automated whitelist update" echo "Changes committed in $BASEDIR" else echo "No changes to commit in $BASEDIR" fi git push echo "updating firewall in 10 sec" sleep 10 cd $FIREWALL_DIR ./install_fw2.sh
Never underestimate the word ethic of a lazy system administrator!