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
↓
terriRisk:
-
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 labuserLocked password:
sudo passwd -l labuserPurpose:
-
Browser terminal no longer runs as
terri -
Dedicated low-privilege account for ttyd
Phase 2 - Shared Group Access
Existing group:
lab
├── terri
└── codeserverAdded:
sudo usermod -aG lab labuserFinal:
lab
├── terri
├── codeserver
└── labuserPurpose:
-
Allow shared access to
/srv/lab -
Avoid permission complexity
Phase 3 - Create Startup Script
Created:
/home/labuser/start.shContents:
#!/bin/bash
cd /srv/lab || exit 1
exec /bin/bash --noprofile --norc -iPermissions:
sudo chown labuser:labuser /home/labuser/start.sh
sudo chmod 700 /home/labuser/start.shPurpose:
-
Ensure ttyd always starts in
/srv/lab -
Avoid loading user configuration files
-
Fail immediately if
/srv/labbecomes inaccessible
Phase 4 - Run ttyd as labuser
Modified:
/etc/systemd/system/ttyd.serviceChanged:
User=labuser
Group=labuserConfigured:
ExecStart=/usr/bin/ttyd \
-i 127.0.0.1 \
-p 7681 \
-W \
/home/labuser/start.shImportant discovery:
-W = writableUsing lowercase:
-wcaused:
ttyd: missing start commandand 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 modeSolution:
-Wor
--writablePhase 5 - Systemd Sandboxing
Added:
NoNewPrivileges=yes
PrivateTmp=yesPurpose:
NoNewPrivileges
Prevents privilege escalation from the ttyd process.
ttyd
↓
cannot gain more privilegesPrivateTmp
Provides isolated temporary directory.
ttyd
↓
private /tmpReduces risk of:
-
snooping
-
symlink attacks
-
temporary file interference
Deferred:
ProtectHome=yes
ProtectSystem=strictReason:
Potentially disruptive to current setup.
Phase 6 - Fail2Ban
Installed:
sudo apt install fail2banCreated:
/etc/fail2ban/jail.localConfiguration:
[nginx-http-auth]
enabled = true
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 5
findtime = 10m
bantime = 1hResult:
5 failed logins
within 10 minutes
↓
ban IP for 1 hourVerification:
sudo fail2ban-client status nginx-http-authStatus confirmed operational.
Workspace Permissions
Current:
ls -ld /srv/labResult:
drwxrws---Meaning:
owner = rwx
group = rwx
others = ---Accessible only to:
terri
codeserver
labuser
rootcode-server Security Review
Current service account:
codeserver
├── codeserver
├── users
└── labNo membership in:
sudo
docker
adm
lxdCurrent service:
User=%i
WorkingDirectory=/srv/lab
ExecStart=/usr/bin/code-server /srv/labAssessment:
-
Similar privilege level to
labuser -
Larger application attack surface than ttyd
-
Candidate for future sandboxing:
NoNewPrivileges=yes
PrivateTmp=yesCurrent Architecture
Internet
↓
HTTPS
↓
Nginx
↓
Basic Auth
↓
Fail2Ban
↓
ttyd
↓
labuser
↓
/srv/labAdministrative access remains separate:
SSH
↓
terri
↓
sudoConclusions
Verified Facts:
-
ttyd now runs as
labuser -
labuserhas no sudo access -
labuserhas no docker access -
Fail2Ban protects Nginx Basic Auth
-
/srv/labrestricted to members oflab -
code-server already runs under dedicated user
codeserver
Security Improvement:
Before:
Browser → terri
After:
Browser → labuserThis significantly reduces the blast radius of a ttyd compromise.
Future Hardening Tasks
Planned for next weekend:
- Disable interactive login for
labuser
sudo usermod -s /usr/sbin/nologin labuser-
Move
start.shoutside/home -
Evaluate:
ProtectHome=yes
ProtectSystem=strict-
Add sandboxing to code-server
-
Evaluate WireGuard-only access
-
Review Nginx security headers