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