Paste this into your Claude Code session on the droplet. Claude will likely ask to run commands in the terminal and paste the output back.
Ok, today we want to harden SSH on this droplet. Here's the setup:
You're SSH'd in as the claude user with a key. This user has no sudo.
I have a root shell open in the DigitalOcean web Console for anything privileged.
Read config files directly to inspect them. For anything that needs root, give me the commands and I'll paste them into the web Console.
Goal: key-only root login, no password auth anywhere. Specifically:
PermitRootLogin prohibit-password
PasswordAuthentication no
PubkeyAuthentication yes
Don't create a sudo user in this step. Don't restart sshd. I'll do that manually after I test login from a fresh tab.
Walk me through it: inspect the current values, back up the file, make the edits, syntax-check the result, and show me a diff. Stop before the restart and give me a rollback command in case I lock myself out.
Stage 1 · Harden the droplet
Enable the UFW firewall
Claude will likely ask you to run the terminal commands again.
Great, Now we're going to enable the UFW firewall. The rules we want:
- Default policy: deny all incoming
- Default policy: allow all outgoing (so we can make API calls)
- Allow inbound on port 22 (SSH)
- Allow inbound on port 80 (HTTP)
- Allow inbound on port 443 (HTTPS)
Steps:
1. Show me the current UFW status.
2. Add the rules above, but Don't enable UFW yet.
3. Run `sudo ufw show added` and confirm port 22 is in the allow list. If it's not, stop. Enabling UFW without an SSH rule will lock me out.
4. Once you confirm port 22 is there, enable UFW.
5. Show me `sudo ufw status verbose`.
6. If you don't have sudo access/permissions, then give me the commands to run in terminal
Stage 1 · Harden the droplet
Install fail2ban
Again, Claude will likely ask you to run a few commands.
Perfect, Install fail2ban with the default SSH protection. Enable it, start it, and show me the status. Then show me `sudo fail2ban-client status sshd` so I can see it's watching the SSH service. Again same thing, I can run sudo
Stage 1 · Harden the droplet
Enable automatic security updates
Your droplet may already be set up for this, but run it anyway.
This is great. Set up unattended-upgrades to automatically install security patches only (not full distribution upgrades). Confirm it's enabled, then show me the relevant config file so I can see what's configured.
Stage 1 · Harden the droplet
Ask Claude what else to lock down
Thank you, is there anything else that we need to do to lock this droplet down? Remember though, while making any further suggestions, that we are using this droplet to develop software and apps that need to be reachable and need to make api calls, etc. So we don't want to lock this up to an unusable state. We are mitigating risk of hackers and unintended access. Please give me a list if there are recommendations before proceeding with further work.
Stage 1 · Harden the droplet
Whitelist who can SSH
This one is a judgement call. I chose to run only the below; see the video for more context.
Great, Let's just take care of "AllowUsers claude & root. Only named users can SSH at all (whitelist)." for now, I'll reboot after. Please give me the command to reboot as well.
Stage 1 · Harden the droplet
Optional: security status report
Optional, but a nice confirmation that everything took.
Thank you. Let's have you give me a one-screen security status report covering:
- SSH config: PermitRootLogin, PasswordAuthentication, PubkeyAuthentication values
- UFW: status and current rules
- Fail2ban: service status and sshd jail status
- Unattended-upgrades: enabled or not
- Listening ports: output of `sudo ss -tlnp`
If anything looks off/wrong, please flag it for me to review.
Stage 2 · Lock in the rules
Save the hardening context to CLAUDE.md
So future Claude Code sessions respect what you just set up instead of undoing it.
Ok now that we've hardened the droplet to an acceptable degree. We want to make sure future Claude Code sessions on this machine don't undo or break any of what we've done and can work within the parameters that we've set.
Please create a file at ~/.claude/CLAUDE.md with the context from this hardening session. If the ~/.claude directory doesn't exist, create it. After writing the file, confirm Claude Code will read it as user-level guidance for future sessions (If there's a better path for system-wide guidance in the current version of Claude Code, or if there is already a Claude.md file. Either update the file or use a path that fits better with this. Also tell me what you chose and why).
Please include everything that we've done in this session along with any other information that may be pertinent to reference by a future session. Let me know if there are any other decisions that need to be made while creating this .md.
Stage 3 · Connect Git
Create a GitLab (or GitHub) account
I use GitLab because GitHub didn't believe I was a human. Create your account, then come back.
✎ Replace the placeholder values (the CAPS words and anything in angle brackets) with your own before running.
Perfect, now we'll move on to back ups. Check that git is installed on this droplet, then set up the global git identity so my commits are attributed correctly. If it's not installed, please do so:
- user.name: YOUR_NAME
- user.email: YOUR_GITLAB_EMAIL
After setting, run `git config --global --list` and show me the result so I can confirm.
Stage 3 · Connect Git
Generate a GitLab SSH key on the droplet
Back in your Claude Code session.
✎ Replace the placeholder values (the CAPS words and anything in angle brackets) with your own before running.
Ok, now - generate an SSH key on this droplet for connecting to GitLab. Use these settings:
- Type: ed25519
- Filename: ~/.ssh/id_ed25519_gitlab
- Comment: "<CLAUDE@DROPLETNAME>"
- No passphrase, so non-interactive git push works
After generating, show me the contents of the .pub file so I can copy it to GitLab.
Stage 3 · Connect Git
Let Claude add the SSH config block
Claude will likely offer to set up the ~/.ssh/config block at the end of its response. Reply:
Yes
Stage 3 · Connect Git
Add the key to GitLab
Back in GitLab, go to where SSH keys are stored and add the public key you copied from Claude Code.
Stage 3 · Connect Git
Test the GitLab connection
Back in Claude. Run this in the DO web console (it should be blocked there) or have Claude run it in your session (it should succeed there).
ssh -T git@gitlab.com
Stage 4 · First project + backup
Create the starter project
Let's create a starter project for an AI voice agent. Use the directory in opt called voice-agent-starter. The project should include:
- README.md with a brief one-paragraph description: "Voice agent starter for the BradBuilds Systems series. This repo will grow into a full AI phone agent in upcoming videos."
- package.json with name "voice-agent-starter", version "0.1.0", "type": "module", and a "start" script that runs "node src/index.js"
- src/index.js. A single line that logs "Voice agent starter. Ready to build" when run
- .env.example with placeholder lines for ANTHROPIC_API_KEY, TWILIO_ACCOUNT_SID, and TWILIO_AUTH_TOKEN (no real values, just the keys with empty strings)
- .gitignore that excludes:
- node_modules/
- .env and .env.local (but NOT .env.example)
- .DS_Store and *.log
- IDE folders: .vscode/, .idea/
When done, show me the directory tree for this project.
Stage 4 · First project + backup
Create the project directory
Claude will likely ask you to run this command. Run it, then tell Claude you did.
Claude will likely offer nvm vs system-wide. I chose system-wide.
Great please give me the command for running is system wide.
Stage 4 · First project + backup
Create the GitLab project
Back in GitLab: create your project (I named mine starter-voice-agent), set it to Private, uncheck Create README, create it, then grab the Clone with SSH address.
Stage 4 · First project + backup
Initialize git and push
Back in Claude. Replace CLONE_ADDRESS_FROM_GIT with your Clone with SSH address.
✎ Replace the placeholder values (the CAPS words and anything in angle brackets) with your own before running.
Yep, let's git init and stage .gitnore.
Here is the connection - CLONE_ADDRESS_FROM_GIT
After the push completes, run `git remote -v` and `git log --oneline` so I can see the connection and the commit.
Some links above are affiliate or referral links - if you sign up through them I may earn a credit or commission, at no extra cost to you. I only point you at tools I actually use.