Saturday, January 31, 2026

Hetzner VPS initial configuration notes

Having just configured the Oracle cloud server, and having used DigitalOcean for years, Hetzner's interface for initial configuration is closer to DigitalOcean than Oracle cloud. And the price of $3.50/month for 2 CPUs and 4GB of RAM with 40GB of storage is mindblowing compared to DigitalOcean's 2 CPUs with 1 GB of RAM and 25GB of storage for $6/month.

Configuration and management interface is at https://console.hetzner.com/

Create user

I logged into the server using

  $ ssh -i ~/.ssh/KEYNAMEHERE root@SERVERIPHERE
  
and then create a new user and group
# sudo adduser pdg
# sudo adduser pdg sudo
# sudo addgroup pdg_grp
# sudo usermod -aG pdg_grp pdg

The -a (append) and -G (groups) options ensure the user is added to the new group without being removed from their other groups.

Validating,

# groups pdg
pdg : pdg sudo users pdg_grp
# id pdg
uid=1000(pdg) gid=1000(pdg) groups=1000(pdg),27(sudo),100(users),1001(pdg_grp)

SSH

Next, disable root from logging in via SSH. First, copy my public cert from my laptop to the server
$ ssh-copy-id pdg@SERVERIPHERE
On the server I exited the root SSH session and logged back in as pdg:
$ ssh -i ~/.ssh/KEYNAMEHERE pdg@SERVERIPHERE
$ sudo vi /etc/ssh/sshd_config
     PermitRootLogin no
     PubkeyAuthentication yes
     PasswordAuthentication no
     PermitEmptyPasswords no
     AuthorizedKeysFile      .ssh/authorized_keys .ssh/authorized_keys2

$ mkdir ~/.ssh
$ chmod 700 ~/.ssh
$ cat ~/.ssh/authorized_keys
$ sudo systemctl daemon-reload
$ sudo systemctl restart ssh.socket

Updating bash configuration

Added to ~/.bash_aliases
lias vi='vim'

alias s='git status'
alias p='git push'

# do not overwrite existing files
set -o noclobber

alias nothing='docker ps; git pull; git push; git status'
alias ll="ls -hal"
alias ..="cd .."
alias grin="grep -R -i -n --color"
and to ~/.bashrc
# HISTSIZE determines the number of commands remembered in memory during the current session.
# HISTFILESIZE determines the maximum number of lines allowed in the history file
export HISTSIZE=100000
export HISTFILESIZE=200000

export HISTTIMEFORMAT="%h %d %H:%M:%S "

shopt -s histappend
shopt -s cmdhist

UFW = Uncomplicated Firewall

  • 104.210.140.141 = OpenAI, observed 2025-12-04
  • 114.119.147.137 = PetalBot (for Huawei), observed 2025-12-04
  • 156.59.198.136 = bytedance, observed 2025-12-04
Best practice for order of firewall rules is:
  1. Specific DENY rules (blocking known bad actors).
  2. Specific ALLOW rules (allowing trusted hosts/networks).
  3. General ALLOW rules (allowing public services).
  4. General DENY rules (the default policy, often implied).
$ sudo ufw status verbose
Status: inactive
$ sudo nft list ruleset
add rules
$ sudo ufw deny from 156.59.198.136/24
$ sudo ufw deny from 114.119.147.0/24
$ sudo ufw deny from 104.210.140.0/24
$ sudo ufw allow ssh
$ sudo ufw allow 443
$ sudo ufw allow 80
Now I have
$ sudo ufw status numbered
Status: active

     To                         Action      From
     --                         ------      ----
[ 1] Anywhere                   DENY IN     156.59.198.0/24           
[ 2] Anywhere                   DENY IN     114.119.147.0/24          
[ 3] Anywhere                   DENY IN     104.210.140.0/24          
[ 4] 22/tcp                     ALLOW IN    Anywhere                  
[ 5] 443                        ALLOW IN    Anywhere                  
[ 6] 80                         ALLOW IN    Anywhere                  
[ 7] 22/tcp (v6)                ALLOW IN    Anywhere (v6)             
[ 8] 443 (v6)                   ALLOW IN    Anywhere (v6)             
[ 9] 80 (v6)                    ALLOW IN    Anywhere (v6)     

Update OS

Running Ubuntu 24.04.3 LTS, default load is
 System information as of Sat Jan 31 02:24:09 AM UTC 2026
  System load:  0.04              Processes:             123
  Usage of /:   3.0% of 37.23GB   Users logged in:       1
  Memory usage: 5%                IPv4 address for eth0: 65.21.252.29
  Swap usage:   0%                IPv6 address for eth0: 2a01:4f9:c013:5897::1
  
$ sudo apt update
$ sudo apt upgrade

Install Docker

Docker
$ sudo apt update
$ sudo apt install apt-transport-https curl
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
$ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
$ sudo apt update
$ sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
$ sudo systemctl is-active docker
active
as per https://linuxiac.com/how-to-install-docker-on-ubuntu-24-04-lts/ Then add pdg to docker group
$ sudo usermod -a -G docker pdg
as per https://stackoverflow.com/questions/47854463/docker-got-permission-denied-while-trying-to-connect-to-the-docker-daemon-socke/48450294#48450294
$ sudo apt -y install make

Enable Upload to GitHub

$ ssh-keygen
$ cat id_ed25519.pub
Upload public key to my profile, https://github.com/settings/keys

Clone git repo

On the server
$ cd /home/pdg/ui_v8_website_flask_neo4j
$ git clone git@github.com:allofphysicsgraph/ui_v8_website_flask_neo4j.git
On my laptop
$ scp -i ~/.ssh/KEYNAMEHERE neo4j_pdg/plugins/apoc.jar pdg@SERVERIPHERE:/home/pdg
On the server
cd /home/pdg/ui_v8_website_flask_neo4j
echo "UID=$(id -u)" > .env
echo "GID=$(id -g)" >> .env

Build containers

$ cd /home/pdg/ui_v8_website_flask_neo4j
$ make container_build
Which results in
$ docker images
IMAGE                                ID             DISK USAGE   CONTENT SIZE   EXTRA
ui_v8_flask_webserver:latest-amd64   68046aa182c6       3.61GB          872MB       
From my laptop I copy the secrets file
$ scp -i ~/.ssh/KEYNAMEHERE .env.google pdg@SERVERIPHERE:/home/pdg/ui_v8_website_flask_neo4j
On the server I have a few changes to enact,
cd /home/pdg/ui_v8_website_flask_neo4j
mkdir logs
cd certs
openssl genrsa > privkey.pem
openssl req -new -x509 -key privkey.pem > fullchain.pem
openssl dhparam -out dhparam.pem 2048
I then tried running the server using
cd /home/pdg/ui_v8_website_flask_neo4j
make launch_webserver
but encountered some permissions issues that were fixed using
cd /home/pdg/ui_v8_website_flask_neo4j
sudo chown -R pdg:pdg neo4j_pdg/
sudo chown -R pdg:pdg dumping_grounds/
I was then able to successfully run
$ make launch_webserver COMPOSE_FLAGS=--detach
The webserver is then running at SERVERIPHERE on the internet!

DNS

Log into https://account.squarespace.com/domains 
On the page https://account.squarespace.com/domains/managed/allofphysics.com/dns/domain-nameservers change the Name Servers to Hetzner's:
  • helium.ns.hetzner.com
  • hydrogen.ns.hetzner.com
  • oxygen.ns.hetzner.com

On the Hetzner console webpage, select "DNS" and then "Add DNS zone"

Set the domain (allofphysics.com) and "Create an Empty zone." (The other options are "Import Zone file" and "Secondary".)

On DigitalOcean's DNS console, change TTL to 300 for each of the domains.

Since the nameservers are ns1.digitalocean.com, then I will need to change the records in the DigitalOcean Control Panel.

In DigitalOcean's Control Panel, set the "A" records to point to Hetzner's SERVERIPHERE and "NS" to point to Hetzner's

In Hetzner's panel, set "@" and "www" and "*" to Hetzner's SERVERIPHERE.

Certbot

In Hetzner's DNS panel, added TXT record "_acme-challenge" with TTL 300. The "value" will later be provided by certbot

Followed instructions from https://certbot.eff.org/instructions?ws=nginx&os=pip

cd /home/pdg/
sudo apt update
sudo apt upgrade
sudo apt install python3 python3-dev python3-venv libaugeas-dev gcc
sudo python3 -m venv /opt/certbot/
sudo /opt/certbot/bin/pip install --upgrade pip
sudo /opt/certbot/bin/pip install certbot certbot-nginx
sudo ln -s /opt/certbot/bin/certbot /usr/local/bin/certbot
Since my nginx isn't running baremetal, I can't use the recommended sudo certbot certonly --nginx
Instead, I used
sudo certbot certonly --manual --preferred-challenges dns \
--server https://acme-v02.api.letsencrypt.org/directory \
-d derivationmap.net -d www.derivationmap.net \
-d allofphysics.com -d www.allofphysics.com

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Requesting a certificate for derivationmap.net and 3 more
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name:

_acme-challenge.allofphysics.com.

with the following value:

ARANDOMLOOKINGSTRINGHERE

Before continuing, verify the TXT record has been deployed. Depending on the DNS
provider, this may take some time, from a few seconds to multiple minutes. You can
check if it has finished deploying with aid of online tools, such as the Google
Admin Toolbox: https://toolbox.googleapps.com/apps/dig/#TXT/_acme-challenge.allofphysics.com.
Look for one or more bolded line(s) below the line ';ANSWER'. It should show the
value(s) you've just added.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/derivationmap.net/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/derivationmap.net/privkey.pem
This certificate expires on 2026-05-01.
These files will be updated when the certificate renews.

NEXT STEPS:
- This certificate will not be renewed automatically. 
Autorenewal of --manual certificates requires the use of 
an authentication hook script (--manual-auth-hook) but one was not provided. 
To renew this certificate, repeat this same certbot command 
before the certificate's expiry date.

The Google Admin Toolbox page (https://toolbox.googleapps.com/apps/dig/#TXT/_acme-challenge.allofphysics.com) didn't pick up the TXT record, but nslookup did:

$ nslookup -q=TXT _acme-challenge.allofphysics.com hydrogen.ns.hetzner.com
Server:		hydrogen.ns.hetzner.com
Address:	213.133.100.98#53

_acme-challenge.allofphysics.com	text = "ARANDOMLOOKINGSTRINGHERE"

Now https://allofphysics.com/ looks correct and has no errors! (Same for derivationmap.net)

The renewal instructions on https://physicsderivationgraph.blogspot.com/2021/10/periodic-renewal-of-https-letsencrypt.html should be valid when these certs expire?

sudo certbot certonly --webroot \
-w /home/pdg/ui_v8_website_flask_neo4j/certs \
--server https://acme-v02.api.letsencrypt.org/directory \
-d derivationmap.net -d www.derivationmap.net \
-d allofphysics.com -d www.allofphysics.com

Wednesday, January 28, 2026

Oracle cloud configuration for Free Tier

Configuring a VCN (Virtual Cloud Network)

To have the full range of options when setting up the VPS, first create a VCN (virtual cloud network) and Create a Subnet. That way, when you are setting up the VPS (virtual private server) you can select an existing VCN and subnet.

From the user dashboard, on the right-side "Build" menu select "Set up a network with a wizard"

"Create VCN" and then use the wizard for "Create VCN with internet connectivity"

VCN name: pdg_test_vcn

Then "Reserved public IPv4 address" > Create


Configuring my First VPS

From the user dashboard, on the right-side "Build" menu select "Create compute instance" wizard,

For "placement" I selected AD2 (no reason not to go with AD1 or AD3 as far as I can tell)

Under "Advanced Options" I selected "On-demand capacity" out of the options; see https://docs.oracle.com/en-us/iaas/Content/Compute/Concepts/computeoverview.htm#capacity_types for descriptions

Under "Image and shape" the default image is "Oracle Linux 9". My Digital Ocean droplet is currently "Ubuntu 24.04.3 LTS". Oracle offers 8 version of Ubuntu, so I selected 24.04

For "Shape" I selected "virtual machine" (rather than "bare metal")

For "Shape series" I selected "Ampere" (rather than AMD or Intel) and "VM.Standard.A1.Flex" which is eligble for Free Tier.

In the Networking section I set up a Primary VNIC (virtual network interface card) which connects to a VCN (virtual cloud network). This is required for a public IP for the Internet.

Primary VNIC name: pdg_test_VNIC

Primary network: "create new virtual cloud network"

new virtual cloud network name: vcn-20260127-0933_pdg

Subnet: create new public subset; name: subnet-20260127-0933_pdg. CIDR block 10.0.0.0/24

Private

 

Result: Oracle doesn't have capacity in the AD

"Out of capacity for shape VM.Standard.A1.Flex in availability domain AD-1. Create the instance in a different availability domain or try again later.If you specified a fault domain, try creating the instance without specifying a fault domain. If that doesn’t work, please try again later.Learn more about host capacity."

Saturday, January 24, 2026

VPS price comparison September 2024

Other posts in this series:

Netcup

https://www.netcup.com/en

German company. Has US hosting capability.

as of 2024-09-02, lowest cost relevant configuration: "VPS 250 G11s"

3.35 eur is currently $3.71 usd
2 vCore (x86)
2 GB ECC RAM
64 GB SSD
Traffic included
  

Unfortunately, the US-based servers are only available for beefy configurations. The "VPS 250 G11s" is hosted in Germany; see https://www.netcup.com/en/server/vps

Namecheap

https://www.namecheap.com/hosting/shared/

Is not a VPS. Provides disk space and pre-configured web services. Not suitable for running Docker.

hetzner

https://www.hetzner.com/cloud/

Servers hosted in Germany or Finland

CX22 is

2 vCPU
4GB RAM
40 GB SSD

for 4.51 eur ($5 USD) per month

AWS S3

AWS S3 is suitable for static sites only. Dynamic content would need to live in lambdas for on-demand execution.

As per https://www.reddit.com/r/aws/comments/mfbgot/is_aws_a_good_alternative_to_a_20mo_vps_for/

AWS LightSail

AWS VPS is "LightSail"

https://calculator.aws/#/createCalculator/Lightsail

The configuration "Bundle:2GB" features

Storage: 60GB
vCPU: 2
Memory: 2GB
Data Transfer Quota: 3TB
and costs $11.77/month

Google Cloud (GCP)

https://cloud.google.com/compute/vm-instance-pricing#general-purpose_machine_type_family

"e2-standard-2"

2 vCPUs
8GB RAM
for $55.08726/month in Virgina, or $48/month in Iowa. Cheaper if you sign up for 1 or 3 years.

linode

https://www.linode.com/pricing/ as of 2024-11-29 offers

1GB RAM, 25GB storage for $5/month
2GB RAM, 50GB storage for $12/month

ioflood

https://ioflood.com/bare-metal-cloud-server.php

Dual Intel Xeon E5-2695v4 CPUs
36 CPU cores
64GB RAM
960GB NVMe SSD
100TB @ 10Gbps (Inbound unmetered)
for $99/mo monthly

https://manage.ioflood.com/orderform/index.php?form=Max-95v4-September-2024

VPS price comparison January 2026

Other posts in this series:

See hhttps://github.com/allofphysicsgraph/task-tracker/issues/56

DigitalOcean

Currently (as of 2026-01-24) paying $6/month for 2 CPUs, 1GB of RAM, 

Was paying $12/month for 2 CPUs, 2GB of RAM (to support Neo4j)

Hetzner


"CPX11" in US host is 2 vCPUs, 2GB of RAM, 40GB SSD for 5 euros. Doesn't include VAT

Oracle

https://www.oracle.com/cloud/compute/arm/pricing/

https://www.oracle.com/cloud/free/

"AMD Compute Instance" has 1/8 OCPU and 1 GB memory

"Arm Compute Instance" has 4 Arm-based Ampere A1 cores and 24 GB of memory usable as 1 VM or up to 4 VMs Always Free 3,000 OCPU hours and 18,000 GB hours per month

I attempted to use Oracle (see https://physicsderivationgraph.blogspot.com/2026/01/oracle-cloud-configuration-for-free-tier.html) but their free tier didn't have any available instances.

Google Compute Engine


"E2" with 2vCPUs and 2GB of RAM is $37.11/month

https://docs.cloud.google.com/free/docs/free-cloud-features says "1 non-preemptible e2-micro VM instance per month in one of the following US regions: Oregon: us-west1. Iowa: us-central1. South Carolina: us-east1. 30 GB-months standard persistent disk. 1 GB of outbound data transfer from North America to all region destinations (excluding China and Australia) per month."

The e2-micro is 0.25 CPUs and 1GB of RAM.

Microsoft Azure

B2pts v2 = 1GB of RAM, 2 vCPUs for $6.13/month
next step up is 
B2pls v2 = 4GB of RAM, 2 vCPUs for $24.5280/month

$3.8909 per GB/month

AWS Lightsail

"Bundles resources like memory, vCPU, and solid-state drive (SSD) storage into one plan, so budgeting is easy." 
aka AWS simplified using pre-set configurations.

$12/month for 2 GB Memory 2 vCPUs 60 GB SSD Disk 3 TB Transfer

AWS

https://aws.amazon.com/free/
https://aws.amazon.com/free/free-tier-faqs/

2026-01-28: "Free plan eligible instances include: T3.micro, T3.small, T4g.micro, T4g.small, C7i-flex.large, M7i-flex.large" (sourcehttps://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-free-tier-usage.html)

"The free plan allows you to experiment with AWS services and build proof-of-concepts at no cost for up to 6 months until you upgrade to a paid plan."

Friday, January 16, 2026

website unavailable: potentially caused by memory leak of fwupd

I have a VPS that runs Ubuntu 24.04.1 LTS (GNU/Linux 6.8.0-85-generic x86_64). The system was out-of-memory, causing the website to be unavailable. The post documents my troubleshooting and diagnosis.

When I logged into the server with the website not responding, the stats were

 System information as of Fri Jan 16 10:12:48 UTC 2026

  System load:           0.18
  Usage of /:            87.9% of 23.17GB
  Memory usage:          84%
  Swap usage:            0%
  Processes:             117
  Users logged in:       0

I used top to see what processes were causing the load. "Shift+M" sorts by memory used.

top - 10:16:10 up 103 days, 12:20,  1 user,  load average: 0.03, 0.07, 0.03
Tasks: 113 total,   1 running, 112 sleeping,   0 stopped,   0 zombie
%Cpu(s):  2.6 us,  1.9 sy,  0.0 ni, 95.1 id,  0.4 wa,  0.0 hi,  0.0 si,  0.0 st 
MiB Mem :    961.6 total,     68.4 free,    905.6 used,    127.0 buff/cache     
MiB Swap:      0.0 total,      0.0 free,      0.0 used.     56.0 avail Mem 

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                                                  
1546860 username  20   0  962272 179692   1012 S   1.9  18.2 184:29.69 gunicorn                                                 
1546859 username  20   0  953968 171696   1020 S   1.9  17.4 159:19.89 gunicorn                                                 
1578333 root      20   0  689004 144056   4176 S   0.0  14.6   0:10.57 fwupd                                                    
    885 root      20   0 2001968  44176  12744 S   0.0   4.5  55:52.04 dockerd                                                  
    325 root      rt   0  288952  27136   8704 S   0.0   2.8  13:08.03 multipathd                                               
    796 root      20   0 1727032  23024   9088 S   0.0   2.3 120:22.17 containerd                                               
1493204 root      20   0 1850364  19216   4352 S   0.0   2.0  66:57.05 snapd                                                    
1546725 username  20   0   64908  15352    768 S   0.0   1.6   1:07.85 gunicorn                                                 
1565013 do-agent  20   0 1238600  14364   8704 S   0.0   1.5   1:08.77 do-agent                                                 
    277 root      19  -1  124224  13840  12620 S   0.0   1.4  16:00.35 systemd-journal                                          
    847 root      20   0  110012  12800   3456 S   0.0   1.3   0:00.12 unattended-upgr  

The thread (https://github.com/fwupd/fwupd/issues/6948) indicates fwupd (a system daemon to allow session software to update firmware) has had memory leaks.

The post (https://www.reddit.com/r/debian/comments/1gilrlm/howto_device_firmware_updates_with_fwupd/) has commands but I didn't use that.

I used sudo reboot and then, once the system was up, the usage was

 System information as of Fri Jan 16 10:25:22 UTC 2026

  System load:           0.06
  Usage of /:            87.7% of 23.17GB
  Memory usage:          51%
  Swap usage:            0%
  Processes:             108
  Users logged in:       0

I then ran

sudo apt update
sudo apt upgrade

Thursday, December 4, 2025

website unresponsive: diagnostic steps and blocking the AI crawlers

I received an email alert this morning with subject "Wachete error notification" which indicated my website wasn't responsive. 

In a browser I verified that https://allofphysics.com/ is hanging. (No immediate error response.) After a few minutes I got "504 Gateway Time-out nginx/1.17.9"

I ssh'd into the VPS (virtual private server) and ran 
docker ps
to verify the containers were running.

I used
top
to verify the CPU load and memory load. Two instances of gunicorn are using 2% of the CPU each and 10% of the RAM each. That's expected.

Next I logged into the VPS web portal to review system usage for the past 7 days. There is certainly a noticeable change of metrics that started suddenly yesterday:



The last interaction I had with the server (more than a week ago) was to update the HTTPS certificates using Let's Encrypt. Although the website had returned a 504 error I could check the certificate expiration in the browser. The certs were valid.

In the logs directory on the server 

-rwxrwxrwx 1 usr  usr          0 Sep  3  2024 auth.log*
-rw-r--r-- 1 usr  usr    6194913 Dec  3 14:38 flask_critical_and_error_and_warning.log
-rw-r--r-- 1 usr  usr     383596 Nov 26 06:27 flask_critical_and_error_and_warning.log.1
-rw-r--r-- 1 usr  usr    9999931 Nov 23 18:02 flask_critical_and_error_and_warning.log.2
-rw-r--r-- 1 usr  usr    9945758 Dec  3 14:42 flask_critical_and_error_and_warning_and_info.log
-rw-r--r-- 1 usr  usr    9999983 Dec  2 16:14 flask_critical_and_error_and_warning_and_info.log.1
-rw-r--r-- 1 usr  usr    9999938 Dec  1 16:40 flask_critical_and_error_and_warning_and_info.log.2
-rw-r--r-- 1 usr  usr    1206714 Dec  3 14:42 flask_critical_and_error_and_warning_and_info_and_debug.log
-rw-r--r-- 1 usr  usr    9999916 Dec  3 12:46 flask_critical_and_error_and_warning_and_info_and_debug.log.1
-rw-r--r-- 1 usr  usr    9999926 Dec  3 00:53 flask_critical_and_error_and_warning_and_info_and_debug.log.2
-rw-r--r-- 1 usr  usr  125459598 Dec  3 14:42 gunicorn_access.log
-rw-r--r-- 1 usr  usr  166722892 Dec  3 14:42 gunicorn_error.log
-rw-r--r-- 1 root root 126147128 Dec  4 11:01 nginx_access.log
-rw-r--r-- 1 root root  28785863 Dec  4 11:01 nginx_error.log
Only nginx logs have today's date. That's consistent with the blocker being nginx. Using
tail -f nginx_access.log
I see the latest entries are associated with https://webmaster.petalsearch.com/site/petalbot which says the crawler
"establish an index database which enables users to search the content of your site in Petal search engine and present content recommendations for the user in Huawei Assistant and AI Search services"

Using Gemini 2.5 Flash from https://aistudio.google.com/ I ask

I'm running a webserver that uses nginx and runs on linux. I am interested in blocking certain IP address ranges. Should I configure nginx to filter IP ranges or should I filter using the linux firewall? I want to use the software I already have rather than add yet another tool for this blocking.
and learn that linux firewall is recommended over nginx.

Next question for Gemini 2.5 Flash LLM is

I'm using Ubuntu for a webserver. How do I determine what firewall is being used from the command line?

I then run the following on my VPS:

$ sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp (OpenSSH)           ALLOW IN    Anywhere                  
443                        ALLOW IN    Anywhere                  
80                         ALLOW IN    Anywhere                  
22/tcp (OpenSSH (v6))      ALLOW IN    Anywhere (v6)             
443 (v6)                   ALLOW IN    Anywhere (v6)             
80 (v6)                    ALLOW IN    Anywhere (v6)             
I then also verify that nft exists using
$ sudo nft list ruleset
# Warning: table ip filter is managed by iptables-nft, do not touch!
table ip filter {
	chain ufw-before-logging-input {
	}
...long output, snipped...
From a few minutes of reviewing tail -f nginx_access.log the major offenders for this denial-of-service (DOS) attack appear to be
104.210.140.141 = OpenAI, observed 2025-12-04
114.119.147.137 = PetalBot (for Huawei), observed 2025-12-04
156.59.198.136 = bytedance, observed 2025-12-04

LLM query:

ufw block IP address range for web server
followed by
how to pick the correct CIDR value for IP blocking?
from which I learn /24 is the last octet (0 to 255)

I then run

$ sudo ufw deny from 156.59.198.136/24
WARN: Rule changed after normalization
Rule added
$ sudo ufw deny from 114.119.147.0/24
Rule added
$ sudo ufw deny from 104.210.140.0/24
Rule added
Check the results
$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere                  
443                        ALLOW       Anywhere                  
80                         ALLOW       Anywhere                  
Anywhere                   DENY        156.59.198.0/24           
Anywhere                   DENY        114.119.147.0/24          
Anywhere                   DENY        104.210.140.0/24          
OpenSSH (v6)               ALLOW       Anywhere (v6)             
443 (v6)                   ALLOW       Anywhere (v6)             
80 (v6)                    ALLOW       Anywhere (v6)             
The LLM had warned me that "If you find that the new deny rules are at the bottom of the list, you may need to use the insert function to put them at the top (e.g., position 1 and 2)." 
Gemini 2.5 says the general best practice for firewall rules is:
  1. Specific DENY rules (blocking known bad actors).
  2. Specific ALLOW rules (allowing trusted hosts/networks).
  3. General ALLOW rules (allowing public services).
  4. General DENY rules (the default policy, often implied).
Gemini 2.5's advice was almost correct. The LLM got the rule indices wrong. Here are the commands I ran:
$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere                  
443                        ALLOW       Anywhere                  
80                         ALLOW       Anywhere                  
Anywhere                   DENY        156.59.198.0/24           
Anywhere                   DENY        114.119.147.0/24          
Anywhere                   DENY        104.210.140.0/24          
OpenSSH (v6)               ALLOW       Anywhere (v6)             
443 (v6)                   ALLOW       Anywhere (v6)             
80 (v6)                    ALLOW       Anywhere (v6)             

$ sudo ufw status numbered
Status: active

     To                         Action      From
     --                         ------      ----
[ 1] OpenSSH                    ALLOW IN    Anywhere                  
[ 2] 443                        ALLOW IN    Anywhere                  
[ 3] 80                         ALLOW IN    Anywhere                  
[ 4] Anywhere                   DENY IN     156.59.198.0/24           
[ 5] Anywhere                   DENY IN     114.119.147.0/24          
[ 6] Anywhere                   DENY IN     104.210.140.0/24          
[ 7] OpenSSH (v6)               ALLOW IN    Anywhere (v6)             
[ 8] 443 (v6)                   ALLOW IN    Anywhere (v6)             
[ 9] 80 (v6)                    ALLOW IN    Anywhere (v6)             

$ sudo ufw delete 4
Deleting:
 deny from 156.59.198.0/24
Proceed with operation (y|n)? y
Rule deleted
$ sudo ufw insert 1 deny from 156.59.198.0/24 to any
Rule inserted
$ sudo ufw delete 4
Deleting:
 allow 80
Proceed with operation (y|n)? n
Aborted
$ sudo ufw status numbered
Status: active

     To                         Action      From
     --                         ------      ----
[ 1] Anywhere                   DENY IN     156.59.198.0/24           
[ 2] OpenSSH                    ALLOW IN    Anywhere                  
[ 3] 443                        ALLOW IN    Anywhere                  
[ 4] 80                         ALLOW IN    Anywhere                  
[ 5] Anywhere                   DENY IN     114.119.147.0/24          
[ 6] Anywhere                   DENY IN     104.210.140.0/24          
[ 7] OpenSSH (v6)               ALLOW IN    Anywhere (v6)             
[ 8] 443 (v6)                   ALLOW IN    Anywhere (v6)             
[ 9] 80 (v6)                    ALLOW IN    Anywhere (v6)             

$ sudo ufw delete 5
Deleting:
 deny from 114.119.147.0/24
Proceed with operation (y|n)? y
Rule deleted
$ sudo ufw insert 2 deny from 114.119.147.0/24 to any
Rule inserted
$ sudo ufw status numbered
Status: active

     To                         Action      From
     --                         ------      ----
[ 1] Anywhere                   DENY IN     156.59.198.0/24           
[ 2] Anywhere                   DENY IN     114.119.147.0/24          
[ 3] OpenSSH                    ALLOW IN    Anywhere                  
[ 4] 443                        ALLOW IN    Anywhere                  
[ 5] 80                         ALLOW IN    Anywhere                  
[ 6] Anywhere                   DENY IN     104.210.140.0/24          
[ 7] OpenSSH (v6)               ALLOW IN    Anywhere (v6)             
[ 8] 443 (v6)                   ALLOW IN    Anywhere (v6)             
[ 9] 80 (v6)                    ALLOW IN    Anywhere (v6)             

$ sudo ufw delete 6
Deleting:
 deny from 104.210.140.0/24
Proceed with operation (y|n)? y
Rule deleted
$ sudo ufw insert 3 deny from 104.210.140.0/24 to any
Rule inserted
$ sudo ufw status numbered
Status: active

     To                         Action      From
     --                         ------      ----
[ 1] Anywhere                   DENY IN     156.59.198.0/24           
[ 2] Anywhere                   DENY IN     114.119.147.0/24          
[ 3] Anywhere                   DENY IN     104.210.140.0/24          
[ 4] OpenSSH                    ALLOW IN    Anywhere                  
[ 5] 443                        ALLOW IN    Anywhere                  
[ 6] 80                         ALLOW IN    Anywhere                  
[ 7] OpenSSH (v6)               ALLOW IN    Anywhere (v6)             
[ 8] 443 (v6)                   ALLOW IN    Anywhere (v6)             
[ 9] 80 (v6)                    ALLOW IN    Anywhere (v6)            

 

In a web browser I visited https://allofphysics.com/ and the page loaded immediately. Yay!

Sunday, November 23, 2025

a single step fully verified using SymPy and Lean

As very simple example, start with the equation

T = 1/f 
where T is period of oscillation and f is frequency of oscillation. A transformation would be to multiply both sides by f to get
f T = 1

Verification of a step using a Computer Algebra System like SymPy

The single step above could be verified using a Computer Algebra System like SymPy. The generic form of the inference rule is "multiply both sides of (LHS=RHS) by feed to get (LHS*feed=RHS*feed)". To show the inference rule was correctly applied, we want to show that

LHS_in*feed == LHS_out
and
RHS_in*feed == RHS_out
Another way to describe the equivalence is that the difference should be zero:
LHS_in*feed - LHS_out = 0
and
RHS_in*feed - RHS_out = 0
That's the generic formulation of the inference rule check. In this step,
LHS_in = T
RHS_in = 1/f
feed = f
LHS_out = f T
RHS_out = 1

This check is implemented in line 496 of validate_steps_sympy.py as

import sympy
def multiply_both_sides_by(LHS_in, RHS_in, feed, LHS_out, RHS_out):
    diff1 = sympy.simplify(sympy.Mul(LHS_in, feed) - LHS_out)
    diff2 = sympy.simplify(sympy.Mul(RHS_in, feed) - RHS_out)
    if (diff1 == 0) and (diff2 == 0):
        return "valid"
    else:
        return "LHS diff is " + str(diff1) + "\n" + "RHS diff is " + str(diff2)
We can run that using
>>> import sympy
>>> print(sympy.__version__)
1.11.1
>>> f, T = sympy.symbols('f T')
>>> multiply_both_sides_by(T, 1/f, f, f*T, 1)
'valid'

Wahoo! The step has been validated using SymPy to show the inference rule is applied correctly.

The cool part is that the "multiply_both_sides()" Python function is generic to any input expression. The same check can be used for many different steps that use the inference rule. Using SymPy we can gain confidence that the expressions associated with a step were correctly transformed. 


Consistency of dimensionality using SymPy

In addition to evaluating the transformation of symbols in a step, we can verify the consistency of dimensions for each expression. That requires more than just the symbols -- the user will have to specify the dimensions of each symbol.

For example, in JSON for period T we have

        "9491": {
            "category": "variable",
            "dimensions": {
                "amount of substance": 0,
                "electric charge": 0,
                "length": 0,
                "luminous intensity": 0,
                "mass": 0,
                "temperature": 0,
                "time": 1
            },
            "latex": "T",
            "name": "period",
            "scope": [
                "real"
            ]
        },

The script validate_dimensions_sympy.py

>>> import sympy
>>> from sympy.physics.units import mass, length, time, temperature, luminous_intensity, amount_of_substance, charge  # type: ignore
>>> from sympy.physics.units.systems.si import dimsys_SI

>>> from sympy.parsing.latex import parse_latex
>>> sympy.srepr(parse_latex('T = 1/f'))

TODO
>>> determine_consistency = dimsys_SI.equivalent_dims( eval(str(LHS)), eval(str(RHS)) )

See https://physicsderivationgraph.blogspot.com/2020/07/function-latex-for-sympy.html

Verification of the step using Lean

To prove
(T=1/f) -> (f*T=1)

additional constraints are needed for reasoning around division by 0. If you expect to avoid negative or zero frequency or period, you could define f and T to have be "positive real numbers" (which exclude zero; non-negative reals include zero). This does define the context more precisely, but there is a price - we won’t have nearly as many proofs for positive reals as we have for reals. The alternative is to add additional hypotheses as constraints. The latter case (additional hypotheses) is favored.

import Mathlib.Data.Real.Basic
import Mathlib.Tactic -- Import standard tactics, specifically import Mathlib.Tactic.FieldSimp

theorem inversion_amultbeq1
  (a b : Real)
  (hb_ne_zero : b ≠ 0) :
  a = 1 / b <-> a * b = 1 := by
  -- field_simp clears the denominator 'b' on the LHS,
  -- turning (a = 1 / b) into (a * b = 1) automatically.
  field_simp [hb_ne_zero]