Skip to content

Latest commit

 

History

History
373 lines (268 loc) · 17.3 KB

File metadata and controls

373 lines (268 loc) · 17.3 KB

CodeGuard — detailed reference

Every per-feature deep dive: gate setup, anti-spam, honeypot, session recording, JIT grants, alerts, files, recovery, dashboard.

The main README has the quickstart and the elevator pitch; come here when you're configuring a feature.


SFTP gate (out-of-band approval)

profile.d only fires for interactive shells, so SFTP/SCP would otherwise slip past CodeGuard. The sftp-gate subcommand plugs that gap: SFTP is denied by default, and the user must grant a single-use approval from an already-authenticated SSH session.

Wire it into /etc/ssh/sshd_config:

Subsystem sftp /usr/local/sbin/codeguard.py sftp-gate

Then systemctl restart ssh. Workflow:

# 1. SSH in (passes CodeGuard via profile.d):
ssh you@host

# 2. Grant a 60-second SFTP window (single-use):
codeguard approve-sftp 60

# 3. From another terminal, within the window:
sftp you@host          # ✓ allowed; approval is consumed
sftp you@host          # ✗ blocked again

Approval state lives in /run/codeguard-sftp-approved (mode 0600) and is auto-deleted on first use or expiry. Each accept / deny is recorded to /var/log/codeguard.log with action, user, IP, and reason.

One-tap approval via Telegram

If the telegram webhook is configured in /etc/codeguard/config.json, enable the bot:

systemctl enable --now codeguard-bot

Now any unapproved SFTP attempt sends a Telegram message with [✅ Approve] [❌ Deny] buttons. Tap from your phone — sftp-gate waits up to 30 seconds for the verdict and either execs sftp-server or denies. No need to SSH in first.

🔐 CodeGuard SFTP request

Host: vps
User: root
IP:   203.0.113.45
Expires in: 30s

[ ✅ Approve ]  [ ❌ Deny ]

Per-approval state lives in /run/codeguard-approvals/<token>.{pending,approved,denied} (mode 0700 directory).

Tailscale SSH bypasses OpenSSH entirely — including this Subsystem hook. If Tailscale SSH is enabled on the host, disable it (tailscale set --ssh=false) before relying on the gate.


Exec gate — close the ssh host 'cmd' bypass

profile.d only fires for interactive logins, so plain ssh host 'cmd', scp, rsync, and any other non-interactive SSH channel would otherwise slip past CodeGuard. exec-gate is the matching ForceCommand hook for the SFTP Subsystem hook.

Wire it into /etc/ssh/sshd_config:

ForceCommand /usr/local/sbin/codeguard.py exec-gate

Then systemctl restart ssh. Behavior:

  • Interactive shell (no SSH_ORIGINAL_COMMAND) — exec the user's login shell; profile.d/CodeGuard prompt runs as before.
  • internal-sftp / sftp-server — fall through (the Subsystem hook handles approval).
  • Anything else — require approval: pre-granted window from codeguard approve-exec, or Telegram inline [✅ Approve] [❌ Deny] (same bot daemon as SFTP). The Telegram prompt includes the requested command (truncated to 200 chars) so the approver can see what's being authorized.

⚠ Once ForceCommand is enabled, all non-interactive automation (CI deploys, rsync backups, git over ssh) needs approval. The pragmatic pattern for one-off commands: SSH in interactively, run codeguard approve-exec 30, then fire the one-shot command from another terminal within the window.


Anti-spam protections

If an attacker captures valid SSH credentials they can otherwise spray ssh host 'cmd' and bombard the approver with Telegram prompts — fishing for an accidental tap. CodeGuard pushes back at three layers:

layer trigger effect
Rate limit > N attempts in the window (default 5 / 10 min per IP) auto-deny, no Telegram message sent
Deny cooldown approver tapped ❌ Deny that IP is silent for N min (default 5)
🚫 Block 1h button third inline button on every prompt banhammer — IP locked out for 1 h

Tune in /etc/codeguard/config.json:

"approval_security": {
  "rate_limit": { "max": 5, "window": 600 },
  "deny_cooldown": 300,
  "block_duration": 3600
}

Admin overrides:

codeguard list-blocked      # see who's rate-limited
codeguard unblock 1.2.3.4   # clear state for an IP

State lives in /var/lib/codeguard/ratelimit.json (mode 0600).


Honeypot — drop attackers into a fake shell

If someone gets through your SSH password but doesn't know the CodeGuard secret, denying them and disconnecting is the boring outcome. After 3 wrong-secret attempts from the same IP, CodeGuard instead routes the next login into a fake shell — Ubuntu banner, plausible prompt, fake ls/cat/ps/id/uname output, virtual CWD, and a sudo that always says "Sorry, try again." Every command they type is appended to /var/log/codeguard-honeypot.log, and Telegram pings you the moment they start (and again whenever they touch interesting things — sudo, wget, curl, /etc/shadow, id_rsa, nmap, …).

codeguard honeypot list             # who's trapped right now
codeguard honeypot add 1.2.3.4 14   # manually route an IP into the trap for 14 days
codeguard honeypot remove 1.2.3.4   # release

The fake shell session is capped at 50 commands. On exit (or exit/Ctrl+D), CodeGuard returns exit code 1 so the existing profile.d hook SIGHUPs the parent shell — the attacker doesn't pop back into a real bash, they just see "connection closed".

Every Telegram alert from the honeypot carries a [🚫 Block & Kick] button. Tap it (or run codeguard kick <ip> from a shell) to:

  • drop a kick flag at /run/codeguard-kicks/<ip> — the live fake shell exits on its next loop tick with "Connection reset by peer",
  • extend the honeypot mark for that IP to 30 days,
  • call fail2ban-client set codeguard banip <ip> so the next connection from that IP is dropped at the firewall.

State at /var/lib/codeguard/honeypot.json (mode 0600); session transcript at /var/log/codeguard-honeypot.log.


Session recording (TTY → asciinema)

codeguard recording on makes every interactive SSH login (including JIT grant sessions) wrap the user's shell in a PTY recorder. Each session is saved as an asciinema v2 JSON file under /var/log/codeguard-sessions/ and shown in the dashboard's Session recordings panel — ▶ play opens an in-browser replay you can scrub.

While the user types, CodeGuard parses each command against a regex catalogue and tags interesting events:

🔐 sudo …
📂 rm/mv/chmod/chown, > file redirects
📝 nano/vim/emacs/code
⚙️ systemctl start/stop/restart/enable
📦 apt/dnf/pacman/pip install
🌐 curl/wget/fetch
👥 useradd/usermod/passwd
🌿 git commit/push/reset
🔥 iptables/ufw/nft

Every flagged command fires a Telegram alert in real time with a [🔚 End session] inline button — you don't have to wait for the session to end to see what they're doing. Events also appear in the player as a click-to-seek timeline.

codeguard recording on        # auto-record every SSH login
codeguard recording status    # → on / off
codeguard recording off       # back to plain login shell

Audit log also goes to syslog with tag codeguard-grant-<user> for grant sessions:

journalctl -t codeguard-grant-cg_alice -n 50

JIT grants — time-limited Linux users with a real namespace jail

The Teleport-style "give the contractor 4 hours of staging" workflow, in one command:

codeguard grant alice --hours 4 --mode readonly --path /var/www/html

The grant:

  • Creates a Linux user cg_alice with a one-time random password (printed once on stdout — copy and hand off via Signal/Telegram/etc.).
  • Adds her to the cg_grants group so the tight sudoers rule applies (one fixed command, no wildcards).
  • Generates a per-grant shell wrapper under /usr/local/lib/codeguard/shells/.
  • A systemd timer (codeguard-grant-tick.timer) sweeps every minute and runs codeguard grant tick, which userdel -rs anything past its expiry and kills its open sessions.
  • Telegram alerts on create, on login, and on expire/revoke. The login alert carries a [🔚 Revoke now] inline button.

Modes

Mode Allowed commands Writable?
readonly ls, cat, tail, head, less, grep, find, pwd, stat, df, du, free, ps, …
deploy readonly + git, systemctl, journalctl, docker, kubectl, rsync, npm, make, ansible, pip, python ✅ target path only
full plain bash

--path DIR → real namespace isolation

With bubblewrap installed (auto-installed by install.sh), the grant shell launches the user inside a per-session namespace jail:

  • /data is the only writable / readable-real-content path (bind-mounted from your --path argument)
  • /usr, /lib, /bin, /sbin are read-only mounts of the host's system tree (so bash and the allowed binaries work)
  • Synthetic /etc/passwd + /etc/group (just root + the cg_* user) — no other users visible
  • New PID / UTS / IPC / user namespaces; --die-with-parent
  • no_new_privs is set, so any sudo / setuid attempt inside the jail is rejected by the kernel
  • Tab completion cannot enumerate outside the jail — the host filesystem isn't there

What that looks like from the grant user's side:

cg_alice@jail:~$ pwd
/data
cg_alice@jail:~$ ls /etc
group  passwd
cg_alice@jail:~$ cat /etc/shadow
cat: /etc/shadow: No such file or directory
cg_alice@jail:~$ cd /var
bash: cd: /var: No such file or directory
cg_alice@jail:~$ sudo whoami
sudo: The "no new privileges" flag is set, which prevents sudo from running as root.

Ubuntu 24.04+ restricts unprivileged user namespaces. After apt install bubblewrap, also run chmod u+s /usr/bin/bwrap (same approach Flatpak uses). The installer offers to do this; codeguard grant ... --path warns + falls back to a soft fence if the setuid bit is missing.

Admin commands

codeguard grant list                       # show active grants + remaining time
codeguard grant extend alice +2h           # add 2 more hours
codeguard grant revoke alice               # kill now (userdel -r + kill sessions)
codeguard grant tick                       # internal: expire-sweep (cron / timer calls this)
codeguard grant is-active cg_alice         # exit 0 iff still active

State persists in /etc/codeguard/grants.json (0600). Per-grant shell wrappers + synthetic passwd files live in /usr/local/lib/codeguard/shells/.


Network-level ban via fail2ban

The shipped fail2ban filter matches both legacy FAIL lines and the new sftp-gate DENY / exec-gate DENY lines, so any spammer that trips the approval rate limit also trips the jail (default: 3 fails / 10 min → 24 h iptables ban). Tapping 🚫 Block 1h calls fail2ban-client set codeguard banip <ip> directly, so the network drop applies immediately rather than waiting for the next failed attempt. codeguard unblock <ip> reverses both layers.


Alerts

Edit /etc/codeguard/config.json and set alerts.enabled = true. Use ${VAR} placeholders so credentials live only in /etc/codeguard/env (mode 0400):

"smtp": {
  "enabled": true,
  "host": "smtp.resend.com",
  "port": 465,
  "ssl": true,
  "user": "resend",
  "pass": "${SMTP_PASS}",
  "from": "alerts@your-domain.com",
  "to": ["you@example.com"]
},
"webhooks": [
  {
    "name": "telegram",
    "enabled": true,
    "url": "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage",
    "method": "POST",
    "headers": {"Content-Type": "application/json"},
    "body_template": "{\"chat_id\": ${TELEGRAM_CHAT_ID}, \"text\": \"🔐 CodeGuard {event}{geo}\\nHost: {host}\\nUser: {user}\\nIP: {ip}\\nReason: {reason}\"}"
  }
]

Test:

sudo codeguard alerts-test smtp
sudo codeguard alerts-test telegram

See examples/ for a complete config + env file.


Files

path mode owner purpose
/usr/local/sbin/codeguard.py 0755 root main binary
/usr/local/sbin/codeguard 0755 root symlink
/etc/codeguard/config.json 0600 root settings + secret hash
/etc/codeguard/env 0400 root credentials referenced via ${VAR}
/etc/codeguard/users/*.json 0600 root per-user mode entries
/etc/codeguard/grants.json 0600 root active JIT grants + expiry
/etc/sudoers.d/codeguard-grant 0440 root grant-user sudoers rule (auto-managed)
/etc/profile.d/codeguard.sh 0755 root login hook
/usr/local/lib/codeguard/shells/ 0755 root per-grant shell wrappers + synthetic passwd/group
/usr/local/lib/codeguard/grant-bin/ 0755 root per-mode PATH allowlist (symlinks to allowed binaries)
/var/log/codeguard.log 0640 root plain-text audit (append-only via chattr +a)
/var/log/codeguard.jsonl 0640 root JSON Lines audit (SIEM-friendly)
/var/log/codeguard-honeypot.log 0640 root honeypot session transcripts
/var/log/codeguard-sessions/*.cast 0600 root asciinema session recordings
/var/log/codeguard-sessions/*.events.json 0600 root per-recording flagged events
/var/lib/codeguard/ratelimit.json 0600 root per-IP rate-limit state
/var/lib/codeguard/honeypot.json 0600 root honeypot IP list (auto + manual)
/run/codeguard-approvals/ 0700 root per-token approval pending/approved/denied markers
/run/codeguard-kicks/ 0700 root "kick this IP" flags (live honeypot eject)
/etc/fail2ban/filter.d/codeguard.conf 0644 root fail2ban filter
/etc/fail2ban/jail.d/codeguard.conf 0644 root fail2ban jail
/etc/systemd/system/codeguard-bot.service 0644 root Telegram long-poller
/etc/systemd/system/codeguard-dashboard.service 0644 root web UI
/etc/systemd/system/codeguard-grant-tick.timer 0644 root grant expiry sweep (every minute)

Recovery — if you forget the secret

CodeGuard's own gates will block you from ssh host 'cmd' once exec-gate is wired up, so the recovery commands have to run from the VPS provider's web console (DigitalOcean / Hetzner / Linode / Vultr all ship one):

# Option 1 — recovery code at the prompt (XXXX-XXXX format from setup)
ssh root@your-host          # then enter your recovery code

# Option 2 — wipe config and re-setup
rm /etc/codeguard/config.json
ssh root@your-host          # auto-runs setup on next login

# Option 3 — disable CodeGuard entirely (kills the gate)
rm /etc/profile.d/codeguard.sh
# Also restore stock SFTP if you wired the Subsystem hook:
sed -i 's|^Subsystem sftp /usr/local/sbin/codeguard.py sftp-gate|Subsystem sftp /usr/lib/openssh/sftp-server|' /etc/ssh/sshd_config
systemctl restart ssh

If fail2ban banned your laptop's IP while you were recovering:

fail2ban-client unban --all
codeguard unblock <your-ip>

⚠ Known limitations

  • Tailscale SSH bypasses every CodeGuard gate. Tailscale ships its own SSH server inside tailscaled that does NOT go through OpenSSH sshd. profile.d, Subsystem and ForceCommand are all skipped. If tailscale status shows --ssh enabled on the host, you have to either tailscale set --ssh=false or rely on Tailscale's own ACL action: check 2FA.
  • No FIDO2 / WebAuthn yet. TOTP only. WebAuthn is on the roadmap for v1.11.
  • Ubuntu 24.04 user-namespace lockdown. bubblewrap needs either chmod u+s /usr/bin/bwrap or sysctl kernel.apparmor_restrict_unprivileged_userns=0. The installer prompts; codeguard grant ... --path warns + falls back to soft fence if unfixed.
  • /usr/bin is fully visible inside grant jails. bwrap mounts the host's /usr read-only so bash + binaries work. Alice can ls /usr/src/linux-headers-* and which sudo, but every write fails on the RO mount and sudo/setuid is blocked by no_new_privs. Tighter per-jail bin allowlists are v1.14.
  • PAM integration is via pam_exec.so, not a native module — see pam/codeguard.conf.
  • One-machine product. No federation across hosts yet — each VPS has its own bot, its own grants, its own audit log. Multi-host central approval is v2.0.

Web dashboard

codeguard dashboard starts a single-page HTTP UI: active SSH sessions, honeypot list with remaining time, rate-limited / blocked IPs, recent audit log, full honeypot transcript — auto-refreshing every 5 s. One-click end-session, release, kick, unblock, and SFTP/exec approval-window grants.

Localhost only by default. Reach from your laptop via SSH tunnel:

# On the server
sudo systemctl enable --now codeguard-dashboard

# On your laptop
ssh -L 8088:127.0.0.1:8088 root@your-host
# then open http://127.0.0.1:8088

Real HTML login page — secret + TOTP + Telegram approval, then a cg_session cookie (1 h, HttpOnly, SameSite=Strict). Failed logins are rate-limited per IP (5 / 10 min → cooldown). Logout button revokes the session both server-side and in the browser. No more permanent Basic-Auth credentials sitting in the browser cache.

To expose on Tailscale instead, override the bind address:

ExecStart=/usr/local/sbin/codeguard.py dashboard 8088 --bind 100.x.x.x

Do NOT bind the dashboard to 0.0.0.0 on a publicly reachable host. Only Basic auth + your CodeGuard secret stand between an attacker and admin APIs (end-session, kick, grant approvals, …). Keep it on localhost behind an SSH tunnel, on Tailscale, or behind a reverse proxy with mTLS / IP allowlist.