Hardening lab.domain.com (2026-05-31)

Objective

Reduce the impact of a compromise of the browser terminal (ttyd) by ensuring it no longer runs with the privileges of the primary user account (terri).


Initial State

Internet
   ↓
HTTPS
   ↓
Nginx
   ↓
Basic Auth
   ↓
ttyd
   ↓
terri

Risk:

  • Browser shell ran as terri

  • Access to personal files

  • Access to SSH credentials

  • Larger attack surface if ttyd was compromised


Phase 1 - Create Dedicated User

Created dedicated service account:

sudo adduser labuser

Locked password:

sudo passwd -l labuser

Purpose:

  • Browser terminal no longer runs as terri

  • Dedicated low-privilege account for ttyd


Phase 2 - Shared Group Access

Existing group:

lab
├── terri
└── codeserver

Added:

sudo usermod -aG lab labuser

Final:

lab
├── terri
├── codeserver
└── labuser

Purpose:

  • Allow shared access to /srv/lab

  • Avoid permission complexity


Phase 3 - Create Startup Script

Created:

/home/labuser/start.sh

Contents:

#!/bin/bash
 
cd /srv/lab || exit 1
 
exec /bin/bash --noprofile --norc -i

Permissions:

sudo chown labuser:labuser /home/labuser/start.sh
sudo chmod 700 /home/labuser/start.sh

Purpose:

  • Ensure ttyd always starts in /srv/lab

  • Avoid loading user configuration files

  • Fail immediately if /srv/lab becomes inaccessible


Phase 4 - Run ttyd as labuser

Modified:

/etc/systemd/system/ttyd.service

Changed:

User=labuser
Group=labuser

Configured:

ExecStart=/usr/bin/ttyd \
    -i 127.0.0.1 \
    -p 7681 \
    -W \
    /home/labuser/start.sh

Important discovery:

-W = writable

Using lowercase:

-w

caused:

ttyd: missing start command

and prevented service startup.


ttyd Debugging Notes

Symptom

Terminal displayed:

bash-5.2$

but keyboard input had no effect.

Root Cause

Journal showed:

The --writable option is not set, will start in readonly mode

Solution:

-W

or

--writable

Phase 5 - Systemd Sandboxing

Added:

NoNewPrivileges=yes
PrivateTmp=yes

Purpose:

NoNewPrivileges

Prevents privilege escalation from the ttyd process.

ttyd
  ↓
cannot gain more privileges

PrivateTmp

Provides isolated temporary directory.

ttyd
  ↓
private /tmp

Reduces risk of:

  • snooping

  • symlink attacks

  • temporary file interference

Deferred:

ProtectHome=yes
ProtectSystem=strict

Reason:

Potentially disruptive to current setup.


Phase 6 - Fail2Ban

Installed:

sudo apt install fail2ban

Created:

/etc/fail2ban/jail.local

Configuration:

[nginx-http-auth]
 
enabled = true
port = http,https
logpath = /var/log/nginx/error.log
 
maxretry = 5
findtime = 10m
bantime = 1h

Result:

5 failed logins
within 10 minutes
↓
ban IP for 1 hour

Verification:

sudo fail2ban-client status nginx-http-auth

Status confirmed operational.


Workspace Permissions

Current:

ls -ld /srv/lab

Result:

drwxrws---

Meaning:

owner = rwx
group = rwx
others = ---

Accessible only to:

terri
codeserver
labuser
root

code-server Security Review

Current service account:

codeserver
├── codeserver
├── users
└── lab

No membership in:

sudo
docker
adm
lxd

Current service:

User=%i
WorkingDirectory=/srv/lab
ExecStart=/usr/bin/code-server /srv/lab

Assessment:

  • Similar privilege level to labuser

  • Larger application attack surface than ttyd

  • Candidate for future sandboxing:

NoNewPrivileges=yes
PrivateTmp=yes

Current Architecture

Internet
   ↓
HTTPS
   ↓
Nginx
   ↓
Basic Auth
   ↓
Fail2Ban
   ↓
ttyd
   ↓
labuser
   ↓
/srv/lab

Administrative access remains separate:

SSH
   ↓
terri
   ↓
sudo

Conclusions

Verified Facts:

  • ttyd now runs as labuser

  • labuser has no sudo access

  • labuser has no docker access

  • Fail2Ban protects Nginx Basic Auth

  • /srv/lab restricted to members of lab

  • code-server already runs under dedicated user codeserver

Security Improvement:

Before:
Browser → terri
 
After:
Browser → labuser

This significantly reduces the blast radius of a ttyd compromise.


Future Hardening Tasks

Planned for next weekend:

  1. Disable interactive login for labuser
sudo usermod -s /usr/sbin/nologin labuser
  1. Move start.sh outside /home

  2. Evaluate:

ProtectHome=yes
ProtectSystem=strict
  1. Add sandboxing to code-server

  2. Evaluate WireGuard-only access

  3. Review Nginx security headers