Cloud Server Security Baseline: 9 Hardening Steps Before Launch

A cloud server security baseline: nine hardening steps before launch — SSH keys, least-privilege security groups, MFA, patching, tested restores.

2026-09-18

A cloud server security baseline: nine hardening steps before launch — SSH keys, least-privilege security groups, MFA, patching, tested restores.

The path into a cloud server is usually boring: find the open port 22 → brute force or credential-stuff a weak password → get root → drop a cron job, start mining, or move sideways. A cloud server security baseline exists to make every link in that chain expensive: nothing worth scanning, no way in through passwords, no root to take, and every action left a trace.

The test is not "did we do it" but "can someone else verify it". Pair every control with a command you can run yourself — a baseline is not a document, it is a check you can repeat every month.

The 9 hardening items at a glance (use it as a checklist)

Read as a whole, these nine items do three things: shrink the surface (SSH, firewall), shrink the blast radius (non-root, MFA, least privilege), and make sure you can recover (backups, logs, credential rotation). The table below is the minimum bar, not the target — hit it first, then get stricter. Where you need provider-side controls (WAF, DDoS protection, bastion access), fill the gaps against cloud security products.

#ItemMinimum bar before launchHow to verify
1SSH loginNo root login, no passwords, keys onlysshd -T reports no for both
2Firewall / security groupAdmin ports limited to source IPsNo 0.0.0.0/0 rule on 22/3306/6379
3Accounts and privilegesOne IAM identity per personIAM user list maps to real people
4MFAEnforced on the main account and privileged usersConsole shows it enabled
5PatchingOS monthly, app dependencies quarterlyapt list --upgradable is empty
6Backup and restoreDaily snapshots plus a quarterly restore drillOne successful restore from snapshot
7EncryptionDisk encryption at rest, TLS 1.2+ everywherecurl -I https:// returns 200
8Logs and alertingAt least 90 days retained, alert on new SSH keyslastb -ai is readable and populated
9Secret managementNo credentials in git, stored and rotatableRepo secret scan returns clean

Order matters: SSH key login first, firewall rules second. Doing it the other way round (closing ports before pushing the key) locks you out of your own server, and the only way back is the provider rescue console — usually a human ticket.

Item 1: SSH — turn off password login, keep keys only

SSH brute force is still the number one way into a cloud host. The first control is simple: make the password path not exist.

  1. Generate a key for yourself or the deploy user: ssh-keygen -t ed25519 -C "you@laptop", then push the public key: cat ~/.ssh/id_ed25519.pub | ssh deploy@SERVER "umask 077; mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
  2. Prove the login works in a fresh terminal before touching the config, and keep the current session open.
  3. Back up the config: sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak. Remember that sshd honours the first occurrence of most keywords, so appending a line at the end of the file does nothing — edit the existing line.
  4. Disable root login: sudo sed -i "s/^#*PermitRootLogin.*/PermitRootLogin no/" /etc/ssh/sshd_config
  5. Disable password login: sudo sed -i "s/^#*PasswordAuthentication.*/PasswordAuthentication no/" /etc/ssh/sshd_config. On older distributions the keyword is called ChallengeResponseAuthentication — handle that one too.
  6. Validate the syntax before reloading: sudo sshd -t && sudo systemctl reload ssh (the service is named sshd on RHEL-family systems).
  7. Confirm the effective values: sudo sshd -T | grep -Ei "permitrootlogin|passwordauthentication" should print PermitRootLogin no and PasswordAuthentication no.

Then rate-limit attempts with fail2ban: sudo apt install -y fail2ban && sudo systemctl enable --now fail2ban, and inspect bans with sudo fail2ban-client status sshd.

On "moving SSH to port 2222": it does not make you safer. Public scanners sweep 1–65535, so a custom port only quiets the default log. The real gain comes from source restriction and key-only login.

Item 2: Firewall and security group — deny by default

The network goal is deny by default. The cloud security group is the first wall (traffic is dropped before it reaches the host); ufw or firewalld on the host is the second. Keeping both costs almost nothing.

  • Security group: open 80/443 to 0.0.0.0/0 only; open 22 to your office egress IP; never give 3306 / 5432 / 6379 / 27017 / 9200 a 0.0.0.0/0 rule
  • Debian/Ubuntu host firewall: sudo ufw default deny incoming → sudo ufw allow 22/tcp → sudo ufw allow 80,443/tcp → sudo ufw enable
  • RHEL-family host firewall: sudo firewall-cmd --permanent --add-service=http --add-service=https, then add a rich rule allowing port 22 from a specific source, and finish with sudo firewall-cmd --reload
  • Databases listen on loopback or the private network only: MySQL with bind-address=127.0.0.1; Redis with bind 127.0.0.1 -::1 and protected-mode yes, reached remotely only over the VPC private network plus a security group

Run ufw allow 22/tcp before ufw enable. (ufw allow OpenSSH works too — it is the profile alias on Ubuntu.) Reversing the order drops the current SSH session immediately.

Check two places: local listeners with ss -tulpn | grep LISTEN, and external exposure with nmap -Pn your.server.ip from a different machine. Both should list only the ports you intended to publish.

Item 3: Accounts — one identity per person, no shared root

Treat root as an account belonging to the system, not to people. The target state: every action maps to a specific identity, and one person can be revoked without touching anyone else.

  1. Console side: create IAM users (or the equivalent sub-users) with least-privilege policies scoped to read-only monitoring or to a single instance; delete long-lived access keys on the main account and let CI and operators use roles with temporary credentials valid for 15–60 minutes
  2. Host side: create a deploy user for routine work and grant only the commands it needs via sudo visudo, instead of a blanket NOPASSWD: ALL
  3. One SSH key per person — never copy the same id_rsa around the team. Offboarding is then a single deleted line in authorized_keys
  4. Add a passphrase to the private key and keep it in ssh-agent: ssh-keygen -p -f ~/.ssh/id_ed25519 plus eval "$(ssh-agent -s)"

Precise sudo grants (in /etc/sudoers.d/deploy): deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart myapp.service. Open exactly the command you need, not root on the whole box.

Item 4: MFA — add a real check before destructive actions

Keys and passwords get copied and phished. MFA is the only control that breaks the assumption "a leaked credential works as-is." It does not stop an intrusion; it invalidates a stolen secret.

  • Enable MFA on the main account, and do not rely on SMS alone: SIM swaps and SMS floods are real techniques. Prefer TOTP (Google Authenticator, 1Password, Bitwarden and similar)
  • Enforce by privilege tier: users who can change security groups or delete resources must have it; read-only accounts can wait
  • Store recovery codes offline in a password manager or a sealed printout — not on the same machine, not in the team chat
  • Console login and SSH login are different things: the former uses the console MFA, the latter needs a PAM module such as pam_google_authenticator or an sshd AuthenticationMethods policy

Do not ship a theatrical MFA policy: someone on the team must be able to run the reset flow for a colleague, and that person needs MFA as well. Otherwise one mistyped second factor becomes an outage.

Enforce it in policy rather than by convention: add "Bool": { "aws:MultiFactorAuthPresent": "true" } to the Condition block of the policy so calls without MFA are denied outright.

Item 5: Patching — schedule updates instead of waiting for incidents

Hardening is not a one-off. The rule is short: patch what can be patched today, schedule the rest (releases, reboots) into a fixed window.

  1. OS level: on Debian/Ubuntu run sudo apt update && sudo apt list --upgradable, then install unattended-upgrades and set APT::Periodic::Update-Package-Lists "1"; and APT::Periodic::Unattended-Upgrade "1"; in /etc/apt/apt.conf.d/20auto-upgrades
  2. RHEL family: update manually with dnf update -y, automate with dnf-automatic via sudo systemctl enable --now dnf-automatic.timer
  3. Kernel and app dependencies: kernel updates need a reboot to take effect (check the version with uname -r, reboot in the window); scan dependencies separately with npm audit --production, pip-audit, and govulncheck ./...
  4. Container images: rebuild periodically with docker build --pull. Base image vulnerabilities do not disappear because your application code did not change

Distributions expire: standard maintenance on mainstream releases runs roughly 5 years, after which security updates stop (CentOS 7 ended in June 2024, and Ubuntu 20.04 LTS standard maintenance ended in May 2025). A machine still on those versions needs an upgrade or a commercial extended-support contract.

For a one-off full audit: sudo apt install -y lynis && sudo lynis audit system. Keep the report as evidence, and diff the next one against it.

Item 6: Backups — a snapshot you never restored is a hypothesis

A backup is worth nothing until you restore it. An untested backup is a hypothesis.

  • Automatic snapshots: once or twice a day, keeping 7–14 days; take a manual snapshot with a label before every release or schema change
  • A snapshot is crash-consistent, not application-consistent: InnoDB rolls back on recovery, but logical transactions can be lost. Export databases too: mysqldump --single-transaction --quick -u root -p dbname > /backup/db-$(date +%F).sql.gz (for InnoDB use --single-transaction instead of --lock-tables)
  • Keep a copy off-site: replicate snapshots across regions or sync to object storage, and encrypt the backups themselves — they usually contain a full copy of user data and are the most overlooked exposure
  • Restore drill: each quarter boot a new instance from a snapshot on an isolated network, verify row counts and the newest timestamps, record the real RTO, then destroy the test instance

Put drill results in the runbook. "RTO 2 hours" is a promise; "measured August 2026: snapshot to readable data in 38 minutes" is a number.

Item 7: Encryption — disks at rest, TLS in transit

Encryption has one job: when the disk is stolen, the backup is downloaded, or traffic is read in the middle, the data stays unreadable.

  • At rest: cloud disks and snapshots normally enable provider-side encryption at no extra charge; use a customer-managed key (CMK) for backup buckets, object storage, and database snapshots so keys can be revoked when people leave. CMKs are usually billed per key and per call, so check live pricing on the provider site
  • In transit: HTTPS everywhere, keep only ssl_protocols TLSv1.2 TLSv1.3; in Nginx, redirect 80 to 443 and set HSTS; issue and renew certificates with sudo certbot --nginx -d example.com
  • Encrypt the internal path too: talk over private IPs inside the VPC, force TLS with certificate verification for database connections, and never expose Redis or MySQL ports outward
  • SSH details: set ForwardAgent no, disable unnecessary forwarding, and centralise private keys in ssh-agent instead of scattering copies across machines

Transport encryption is not just the public API. Upload endpoints, webhook callbacks, pre-signed object storage URLs, and third-party SDK certificate-verification switches all need checking — they are often left off in test environments.

Certificate expiry incidents are rarely caused by weak crypto. They are caused by renewal being a human responsibility. Automate it.

Item 8: Logging and alerting — 90 days of trail, alerts on new keys

In an incident there is exactly one dividing line: can you find out what happened three days ago, right now.

  1. Retain system logs: set rotate 14 with daily in /etc/logrotate.d/rsyslog, and check journalctl --disk-usage to confirm the on-disk volume stays under control
  2. Turn on auditd for sensitive file changes: sudo systemctl enable --now auditd, with rules in /etc/audit/rules.d/audit.rules:
  • Rule lines: -w /etc/passwd -p wa -k identity, -w /etc/sudoers -p wa -k sudo, -w /etc/ssh/sshd_config -p wa -k sshd_config
  • Load them: sudo augenrules --load (on RHEL family also run sudo systemctl restart auditd)
  • Investigate later with sudo ausearch -k identity -ts recent
  1. Cloud-side auditing: enable object storage access logs and the provider operation audit service (CloudTrail and equivalents), written by a separate account with a retention policy
  2. Keep only three high-signal alerts: a new SSH public key, a spike in failed logins (brute force from an unfamiliar range), and admin actions outside working hours

Set a floor for retention: 30–90 days online, exported to object storage for a year. For quick triage, last -ai shows successful logins and lastb -ai shows failed ones.

Default fail2ban thresholds can ban an entire office egress range during a network blip. Watch sudo fail2ban-client status sshd for a week before tightening the ban time.

Item 9: Secret management — no credential ever enters the repository

The most common credential leak is not a breach. It is a key committed to git, baked into an image layer, or pasted into a ticket and a chat window.

  1. No secrets in code: add .env to .gitignore. If it was already committed, treat it as compromised — git rm --cached .env only stops future tracking, history needs git filter-repo, and the credential must be rotated at the same time
  2. Scan before committing: gitleaks detect or trufflehog git ., wired into pre-commit or CI
  3. Inject at runtime: supply secrets as environment variables from CI/CD, or keep them in a managed secret service (Secrets Manager, KMS and similar) and fetch them at startup. Do not put them in a Dockerfile ENV — that bakes them into the image
  4. File permissions: chmod 600 .env, create sensitive files under umask 077, run the application as its own system user, and use chown root:appgroup plus chmod 640 where a group needs read access

Command-line arguments show up in ps output. With mysql -uroot -pPASSWORD, any user on that machine can read the password via ps aux. Use the interactive prompt, or mysql --defaults-extra-file=/root/.my.cnf. Also check shell history for export SECRET=....

Give rotation a rhythm: update the database password with ALTER USER together with the application release, rotate third-party API keys quarterly, and record purpose, owner, creation date and expiry for every key. A key with no expiry date is a permanent key.

A key just leaked: the 15-minute response checklist

Assume an SSH private key or a cloud access key just leaked. The first move is revocation — not a password change, and definitely not two days of watching.

  1. Revoke (0–5 min): disable or delete the exposed access key in the IAM console; remove the matching public key from authorized_keys on every server (make sure you still have another way in first, or you will lock yourself out)
  2. Preserve evidence (5–15 min): snapshot the affected instance immediately and export sudo last -ai, lastb -ai, sudo journalctl -u ssh --since "-24h", the current ss -tulpn output, plus cron entries and custom systemd units (/etc/cron*, /etc/systemd/system/)
  3. Hunt (15–60 min): new system users, diffs in authorized_keys, crontab -l, unexpected listening ports, unexpected outbound connections (ss -tunp), and suspicious public key comments
  4. Rotate (in parallel): everything reachable from that machine — database passwords, object storage credentials, third-party API keys, CI/CD secrets, registry tokens. One leak usually means the whole credential set changes
  5. Rebuild rather than clean: if root access is confirmed, cleaning in place is not enough, because persistence may sit where you did not look. Rebuild from a trusted image or a known-good snapshot and migrate only data you have reviewed
  6. Write it up: a timeline (when detected, which credentials were affected, whether they were used), notify the parties involved based on impact, and turn the finding into an alert rule

If the credential was actually used before revocation, handle it as a data breach: work out what data was accessed and whether disclosure is required, instead of closing the ticket as a configuration mistake.

How to verify: a 15-minute self-check

A baseline should be verifiable in 15 minutes. Open another terminal and run these in order:

  1. Effective SSH config: sudo sshd -T | grep -Ei "permitrootlogin|passwordauthentication" — expect no for both
  2. External exposure, from another machine: nmap -Pn your.server.ip — only 22/80/443 plus anything you deliberately opened
  3. Local listeners: ss -tulpn | grep LISTEN — databases and caches should bind to 127.0.0.1 or a private address
  4. Firewall rules: sudo ufw status verbose (or sudo firewall-cmd --list-all) — default policy should deny or allow-list only
  5. Pending updates: sudo apt list --upgradable (Debian family) / dnf check-update (RHEL family)
  6. Failed logins: lastb -ai | head -20 — unfamiliar ranges deserve a follow-up
  7. SUID files and scheduled tasks: sudo find / -xdev -perm -4000 -type f 2>/dev/null, crontab -l, ls -la /etc/cron.d/
  8. Repository secret scan: git ls-files | xargs grep -nEi "akia|secret|password" — any hit is treated as leaked

Put these eight into a monthly routine or a CI baseline job. Automated hardening is a baseline; hardening done once is just a config change.

Next step: run the eight checks against one production machine and split the findings into "fixable in five minutes" and "needs a scheduled window". Clear the first column today, and put the second into the release schedule with an owner and a date. The other half of the risk sits at the account level — accounts of unclear provenance, shared payment instruments, guilt-by-association suspensions — and it is not covered by these nine items: five suspension causes and how to prevent them.

This is a provider-neutral minimum set. Your vendor documentation may be stricter (forcing IMDSv2, default-deny inbound rules, mandatory log retention), so follow the live official documentation for the platform you actually run.

Want to price this for your own usage?

Tell us your region and spec — an engineer will size it and quote. We accept USDT TRC-20.

Top up now