Learn CodeNotes from sessions with Claude Code, my teacher
Lesson 08

Shipping safely to shared hosting

2026-10-07 · about 5 minutes
deploymentsecurityFTP.htaccesshosting

The situation#

The website lives on shared hosting: a server shared with other customers, where you get a folder (public_html), PHP, a database and FTP access, but not full control of the machine. Everything we built had to be uploaded there safely.

1. Uploading: FTP, but encrypted#

FTP (File Transfer Protocol) copies files to the server. Plain FTP sends your password unencrypted, so we always used FTPS: FTP over TLS, the same encryption as https://.

\"No alternative certificate subject name matches\"

The encrypted connection was refused because the server's certificate was issued for the hosting company's own domain (a wildcard like *.provider.example), not for ftp.ourdomain.com. Turning verification off would "work" but would also accept an impostor. Instead we connected to the same server under a name the certificate covers, keeping verification fully on. Lesson: fix certificate mismatches properly; never just disable the check.

Keeping passwords out of the process list

Commands typed with a password in them (curl -u user:pass ...) can be seen by other programs listing running processes. We passed credentials through standard input (curl -K -) instead, so they never appear on the command line.

2. Password-protecting pages: .htaccess#

Apache and LiteSpeed web servers read a file called .htaccess in each folder for rules. To protect just a few pages we added:

<FilesMatch "^(demo\.html|demo-api\.php)$">
  AuthType Basic
  AuthName "Private demo"
  AuthUserFile /full/path/to/private/.htpasswd
  Require valid-user
</FilesMatch>
  • Basic Auth makes the browser show a username/password box.
  • The password is stored hashed in .htpasswd (never as plain text).
  • FilesMatch limits the rule to those files, so the rest of the site is untouched.

\"Unauthorised\" on the phone

The page kept refusing a correct password. Two classic causes: the phone keyboard auto-capitalised the username (Harry ≠ harry), and some in-app browsers never show the login box at all. Lesson: when "it doesn't work" on someone else's device, reproduce from the server side first (our test proved the login was fine), then look at the device.

3. Locking private folders#

Secrets (API keys, database settings, indexes) must be on the server but never downloadable. A folder with this .htaccess refuses all web requests:

<IfModule mod_authz_core.c>
  Require all denied
</IfModule>

PHP scripts can still require files from it, because that happens inside the server, not over the web.

4. Never trust, always test#

After every deploy we ran a checklist and looked at status codes:

Request Expected Meaning
Private page, no password 401 Unauthorised: asks for login
Private page, right password 200 OK
Secret file from the web 403 Forbidden: blocked
The site's homepage 200 We didn't break anything else

A 200 isn't always success

After deleting a temporary test file, its address still returned 200. Looking at the content showed WordPress's "This page does not exist" page: the site returns 200 for its own error page. Always check what came back, not just the code.

5. Backups before every change#

House rule: download the server's current copy of a file before replacing it, and never delete anything that wasn't explicitly asked for. When we edited the site-wide .htaccess we:

  1. Downloaded the original into a dated backup folder.
  2. Appended our block at the end, leaving the WordPress and cache sections untouched.
  3. Uploaded, then tested the homepage and WordPress login still worked.

Upload order matters#

When a deploy includes both a lock and the thing it protects, upload the lock first: folder .htaccess → private files → site rule → public pages. Then nothing is ever exposed, even for a second.

Temporary probe files#

To learn the server's real folder path and PHP version, we uploaded a tiny one-off script with a random name, read its output, and deleted it immediately, then checked the folder listing to confirm it was gone.

6. Automating hosting with an API#

Some tasks (creating a database, adding a subdomain) can't be done over FTP. The hosting company's API can, with a token. We used it to create the tools database and this very subdomain.

A subdomain in the wrong folder

Creating this site's subdomain, we passed the folder as public_html/learn-code, but the API already starts inside public_html, so it became public_html/public_html/learn-code. Caught by reading the API's response, fixed by recreating it with just learn-code. Lesson: read what an API says it did, not what you meant it to do.

Files uploaded, page still \"Default page\"

Even with the subdomain fixed, the site kept showing the host's welcome page for 15+ minutes. The FTP listing showed our files, but a brand-new test file returned "not found" too, so the web server was not reading the folder FTP could see. Asking the host's file API what the subdomain contained revealed only its placeholder: the new subdomain had its own storage that the FTP login doesn't reach. Publishing through the host's official upload + deploy API made it live in 15 seconds. Lessons: when two views of "the same folder" disagree, ask the system that actually serves the site; and prefer the platform's official deploy route over side doors.

Key takeaways#

  • Use encrypted FTP (FTPS) and keep certificate checks on.
  • .htaccess can password-protect specific files and lock whole folders.
  • Secrets live in a web-denied folder; PHP can still read them.
  • Back up first, change the minimum, upload locks before content.
  • Test with status codes and content: 401, 403, 200 (and what that 200 actually contains).

Quick quiz#

1. What's the difference between 401 and 403?

401: you need to log in. 403: you're not allowed, full stop (e.g. a locked folder).

2. Why upload the folder's lock before the secret files?

So there is never a moment when the secrets are on the server without protection.

3. Why not just turn off certificate verification when it fails?

Then the connection would accept any server, including an attacker pretending to be yours.

Try it yourself#

Check a site's status codes

In a terminal, run curl -s -o /dev/null -w "%{http_code}\n" https://example.com/ and then the same with a page that doesn't exist. Compare the codes, then open both in a browser and see what the page actually says.