← Back to Wiki
Self-Hosting / Security

Publish the Numbers Without Publishing the Addresses

You want a status page built on private data. Wallet balances, account numbers, an API key that does the lookups. The page has to show the numbers. It must never show what produced them. That split has to exist on disk, not just in your head. Everything under the web root is public whether you linked it or not.

The web root holds output only

Everything under the web root is served. Not everything you linked. Everything.

So make the rule simple. The web root holds generated output and nothing else.

Put the file with the real identifiers somewhere else. Lock it down with command chmod 600 /opt/app/addresses.json

A sync script reads that file, does the lookups, and writes a second file into the web root with only the numbers in it.

The page fetches the second file.

The page has no idea an address exists anywhere.

Run it on a timer with command crontab -e and the page gets fresh numbers without ever holding a credential.

Your backups are in the web root too

Editing a live page means leaving a backup next to it. Everybody does it.

index.html.bak. index.html.old. index.html.bak-1786763926.

Those get served.

Nothing links them, so nobody stumbles onto one, and search engines will not index them either. None of that makes them private. Anyone who guesses the name gets the entire old page, including whatever you took out of the new one.

Check yours with command curl -s -o /dev/null -w "%{http_code}\n" https://example.com/index.html.bak

A 200 means it is public right now.

See what is really in there with command ls -la /srv/yoursite/

I found six on my own dashboard. Every edit I had made to that page was sitting there, fetchable, going back to the day it was built.

Move them out. Keep them if you want them, just not there.

BE WARNED: a retired data file is still a served data file. When a page stops reading something, move it out of the web root the same day. Leave it and the stale numbers stay fetchable forever. A cached copy of the old page keeps loading them too. So the logs cannot tell you whether anyone is still on the old version.

Prove the old paths are gone

Do not assume the move worked. Ask for every one of them.

for p in /balances.json /onchain.json /index.html.bak; do
  printf "%-24s %s\n" "$p" "$(curl -s -o /dev/null -w '%{http_code}' https://example.com$p)"
done

You want 404 on every line.

Put the retired files somewhere only root can open with command chmod 700 /opt/app/retired

Publish stale, never publish zero

Every one of these lookups goes to something you do not control. Public APIs blip. Rate limits hit. One of mine returns 403 to the default Python user agent and 200 to literally any other string.

So decide now what the page shows when a lookup fails.

Do not show zero.

A balance that reads 0 because an API timed out looks exactly like a balance that really is 0. You will believe it. It is the one wrong answer that does not look wrong.

Keep the last known number instead and mark the row.

try:
    entry["amount"] = lookup(row)
    entry["stale"] = False
except Exception as exc:
    errors.append(f"{row['symbol']}: {exc}")
    entry["amount"] = previous.get(key)
    entry["stale"] = True

Then show that mark on the page, so a stale row looks stale.

Give every lookup a second endpoint

One dead public API should not blank a row that has money in it.

Take a list of endpoints instead of one. Fall through it. Only fail when they all fail.

def try_all(urls, fn):
    last = None
    for u in urls:
        try:
            return fn(u)
        except Exception as exc:
            last = exc
    raise last

Publish the error count alongside the data. Then the page can say two lookups failed, instead of quietly showing eleven rows where there should be thirteen.

Check what you actually published

Read the file you are serving. Not the script that wrote it.

Hunt for anything long and random with command grep -oE '[a-zA-Z0-9]{26,}' /srv/yoursite/data.json

Addresses, keys and tokens are long random strings. Numbers and coin names are not.

If that comes back empty, you published numbers.

Do the same to the page itself before you call it done with command curl -s https://example.com/data.json | grep -oE '[a-zA-Z0-9]{26,}'

Check the served copy, because that is the one strangers get.

Share on X