Skip to content

GeorgeBigh/CodeGuard

Repository files navigation

CodeGuard

A defence-in-depth layer for OpenSSH on Linux — out-of-band approval, honeypot diversion, session recording, and just-in-time access grants — distributed as a single Python file with no runtime dependencies.

CI License Single file


Overview

CodeGuard is a self-contained Python tool (~6,000 lines, standard library only) that sits between OpenSSH and the user's login shell. After OpenSSH authenticates with the user's normal password or key, CodeGuard inserts a second gate: a scrypt-hashed shared secret, optional TOTP, and — for non-interactive channels (SFTP, SCP, ssh host 'cmd') — a one-tap Telegram approval. Repeat wrong-secret IPs are diverted into a logged sandboxed shell; every interactive session is recorded as an asciinema cast; time-limited Linux users can be provisioned with bubblewrap namespace isolation.

Designed for individual operators, homelab environments, and small-team deployments. For SOC 2, HIPAA, or multi-tenant requirements, Teleport remains the appropriate choice.

Installation

git clone https://github.com/GeorgeBigh/CodeGuard.git
cd CodeGuard
sudo ./install.sh

The installer detects your distribution (Debian / Ubuntu / RHEL / Alma / Alpine), copies the single binary to /usr/local/sbin/codeguard.py, sets the profile.d hook, optionally installs the systemd units and the fail2ban filter, and prints a list of next steps. It does not restart sshd automatically — that's done explicitly after you've confirmed your secret works in a second session.

Initial setup

sudo codeguard setup

Interactive flow:

  1. Choose UI language — English or Georgian (ქართული).
  2. Set the prompt timeout (default 30 s).
  3. Enter your access secret twice. This is hashed with scrypt (N=2^17, r=8, p=1) and stored in /etc/codeguard/config.json (mode 0600). Constant-time compare on verification.
  4. Enable TOTP (optional, recommended). Outputs an otpauth:// URI and an ASCII-art QR code; scan with Google Authenticator, Authy, 1Password, or any RFC 6238 client.
  5. Generate and display 10 one-time recovery codes. Each is single-use. Save them in your password manager — they are the only way back in if you forget the secret.

Hardening audit

sudo codeguard doctor

Runs 18 checks against your setup: file permissions, sshd configuration, fail2ban rules, sudoers, systemd hardening flags, recording subsystem, jail integrity, and a few others. Each check prints OK, WARN, or FAIL with an explanation and a remediation hint.

Always keep a second SSH session open and verify you can log in fresh from a separate terminal before disconnecting the first. If anything goes wrong: rm /etc/profile.d/codeguard.sh disables the gate instantly without uninstalling CodeGuard.

How it works

┌─────────────┐    ┌─────────────┐    ┌────────────────────────────┐
│ ssh client  │ ─► │ sshd (auth) │ ─► │ /etc/profile.d/codeguard.sh│
└─────────────┘    └─────────────┘    │   → codeguard verify       │
                                      └────────────┬───────────────┘
                                                   │ ok
                                                   ▼
                                            exec $SHELL
  1. OpenSSH authenticates with the user's normal password or SSH key.
  2. Before the user's login shell starts, /etc/profile.d/codeguard.sh invokes codeguard verify, which prompts for the access secret and (if enabled) TOTP code, performs scrypt + constant-time compare, logs the attempt, and dispatches alerts on failure or foreign-IP success.
  3. On success: exec $SHELL. On failure: log + alert + close connection. On repeat failures from the same IP: divert into the honeypot (see below).

Non-interactive channels — SFTP, SCP, ssh host 'cmd', rsync, git over SSH — bypass profile.d. CodeGuard plugs that gap with two separate sshd hooks, documented in docs/details.md.

Commands reference

The CLI is a single binary; all subcommands are documented below.

Setup & maintenance

Command Purpose
codeguard setup Initial interactive setup. Sets the access secret, optional TOTP, and prints 10 recovery codes. Run once after install.
codeguard setup-user <name> Switch to per-user mode and add a user. Each user gets their own secret/TOTP/recovery codes.
codeguard change Rotate the access secret. Requires the old secret.
codeguard status Print current configuration summary and the last 20 audit-log entries.
codeguard doctor 18-check hardening audit. Exit code 0 if all pass.
codeguard version Print the version.

Auth gates (sshd integration)

Command Purpose
codeguard verify Entry point invoked by /etc/profile.d/codeguard.sh for interactive shells. Prompts for secret + TOTP.
codeguard gate Alternative sshd ForceCommand entry point. Use this if you prefer ForceCommand to profile.d.
codeguard pam-auth PAM exec entry point. Use via pam_exec.so in /etc/pam.d/sshd.
codeguard sftp-gate sshd Subsystem entry point that gates SFTP. Wire into sshd_config.
codeguard exec-gate sshd ForceCommand entry point that gates ssh host 'cmd', SCP, rsync.
codeguard approve-sftp [SECONDS] Grant a single-use SFTP approval window (default 60 s). Used from an already-authenticated shell.
codeguard approve-exec [SECONDS] Same, for ssh-exec / SCP / rsync.

Approval channels (Telegram, alerts)

Command Purpose
codeguard bot Long-polls Telegram for inline-button approvals. Run as a systemd service.
codeguard alerts-init Generate disabled alert template stubs (SMTP, Telegram, WhatsApp, Discord, Slack, ntfy, SMS) in config.json.
codeguard alerts-test <name> Send a real test alert through the named transport. <name> is smtp, telegram, discord, slack, whatsapp-twilio, whatsapp-meta, sms-twilio, sms-ubill, or ntfy.

Rate-limit & block state

Command Purpose
codeguard list-blocked Show IPs currently rate-limited or in deny-cooldown.
codeguard unblock <ip> Clear rate-limit / cooldown / block state for an IP. Also calls fail2ban-client unbanip.
codeguard kick <ip> End any live honeypot session for an IP, extend honeypot mark to 30 days, and fail2ban ban.

Honeypot

Command Purpose
codeguard honeypot list Show currently trapped IPs and remaining time.
codeguard honeypot add <ip> [DAYS] Manually route an IP into the trap (default 14 days).
codeguard honeypot remove <ip> Release an IP from the trap.
codeguard honeypot-shell Internal — auto-invoked by verify when an IP is honeypot-marked.

Session recording

Command Purpose
codeguard recording {on,off,status} Toggle auto-recording for every interactive SSH login.
codeguard recording-enabled Shell-friendly: exit 0 iff auto-recording is enabled. Used by profile.d.
codeguard record-session [SHELL] Manually wrap a single shell with the PTY + asciinema recorder.
codeguard sessions List active SSH child processes (interactive, SFTP, exec).
codeguard end-session <PID|IP> SIGHUP a live session. No ban, no honeypot mark.

JIT user grants

Command Purpose
codeguard grant <user> --hours N --mode {readonly,deploy,full} [--path DIR] Create a time-limited Linux user. With --path, the user's shell runs inside a bubblewrap namespace jail bound to that path.
codeguard grant list Show active grants and remaining time.
codeguard grant extend <user> +Nh Add hours to an active grant.
codeguard grant revoke <user> Revoke a grant immediately. userdel -r plus killing live sessions.
codeguard grant tick Internal — expiry sweep, run every minute by the systemd timer.
codeguard grant is-active <user> Exit 0 iff the grant is still active.

Geofencing

Command Purpose
codeguard geofence list Show current allow/deny policy.
codeguard geofence allow CC[,CC,...] Only allow logins from these ISO-3166 country codes.
codeguard geofence deny CC[,CC,...] Block logins from these countries.
codeguard geofence clear Remove the policy (all countries allowed).

Backup & restore

Command Purpose
codeguard backup --out FILE [--password PASS] Encrypted snapshot (openssl AES-256-CBC + PBKDF2) of state, config, systemd units, sudoers rule, and fail2ban filter.
codeguard restore FILE [--password PASS] Verify and restore an encrypted backup. Prompts for confirmation before overwriting.

Dashboard

Command Purpose
codeguard dashboard [PORT] [--bind ADDR] Start the single-page web UI. Default 127.0.0.1:8088.

Usage walkthrough

1. Configure Telegram approval (5 minutes)

Create a bot on Telegram (@BotFather/newbot). Get the bot token and your chat ID. Add to /etc/codeguard/env (mode 0400):

TELEGRAM_TOKEN=8123456789:AAH...
TELEGRAM_CHAT_ID=987654321

Then enable the alert template in /etc/codeguard/config.json:

"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}\\nHost: {host}\\nUser: {user}\\nIP: {ip}\\nReason: {reason}\"}"
  }
]

Test:

sudo codeguard alerts-test telegram

The message should arrive on Telegram with all placeholders substituted.

2. Wire the SFTP and exec gates (1 minute)

Edit /etc/ssh/sshd_config:

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

Restart sshd (systemctl restart ssh). Now every SFTP / SCP / ssh host 'cmd' attempt requires a Telegram approval or a pre-granted window:

# From a live SSH session:
codeguard approve-sftp 60      # grants a 60-second SFTP window

# Then from your laptop, within the window:
sftp user@host                  # ✓ allowed
sftp user@host                  # ✗ blocked again (single-use)

Once the Telegram bot is running (systemctl enable --now codeguard-bot), the inline-button approval works without needing a pre-grant.

3. Enable session recording

sudo codeguard recording on

Every interactive SSH login from this point is wrapped in a PTY recorder. Casts land in /var/log/codeguard-sessions/ as <id>.cast (asciinema v2 JSON). Flagged events appear in <id>.events.json and ping Telegram in real time:

Pattern Emoji Tag
sudo … 🔐 privilege escalation
rm / mv / chmod / chown / output redirects 📂 destructive fs
nano / vim / emacs / code 📝 editor
systemctl start/stop/restart/enable ⚙️ service control
apt/dnf/pacman/pip install 📦 package install
curl / wget / fetch 🌐 outbound HTTP
useradd / usermod / passwd 👥 user management
git commit/push/reset 🌿 git mutations
iptables / ufw / nft 🔥 firewall changes

The dashboard renders each cast with a click-to-seek event timeline. Audit logs also go to syslog as codeguard-grant-<user>:

journalctl -t codeguard-grant-cg_alice -n 50

4. Issue a time-limited user grant

A contractor needs four hours of read-only access to /var/www/html:

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

What happens:

  1. A Linux user cg_alice is created with a one-time random password (printed once on stdout — hand off via Signal/Telegram).
  2. The user is added to the cg_grants group, which a tight /etc/sudoers.d/codeguard-grant rule references (no wildcards, no privilege escalation).
  3. A per-grant shell wrapper is generated under /usr/local/lib/codeguard/shells/. The wrapper restricts the PATH to the mode allowlist.
  4. If bubblewrap is installed, the shell launches in a per-session namespace jail: only /data (a bind-mount of --path) is writable, /usr/bin is read-only, /etc/passwd and /etc/group are synthetic with just root + cg_alice, and no_new_privs is set so sudo and setuid binaries are rejected by the kernel.
  5. A systemd timer (codeguard-grant-tick.timer) sweeps every minute and runs userdel -r plus killing live sessions when the grant expires.
  6. Telegram pings on create, on login, and on expire / revoke. The login alert carries a 🔚 Revoke now inline button.
Mode Allowed binaries Writable inside jail
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 yes

Manage grants:

codeguard grant list                       # active grants + time left
codeguard grant extend alice +2h           # add 2 hours
codeguard grant revoke alice               # end immediately
codeguard grant is-active cg_alice         # exit 0 if still active

5. Web dashboard

sudo systemctl enable --now codeguard-dashboard

Default bind: 127.0.0.1:8088. From your laptop:

ssh -L 8088:127.0.0.1:8088 root@your-host

Open http://127.0.0.1:8088. Login requires the access secret + TOTP (or WebAuthn / Passkey + Telegram approval). Sessions are HTTP-only cookies (cg_session, 1 hour, SameSite=Strict). The dashboard provides:

  • Live SSH session list with one-click end-session
  • Honeypot transcripts and IP feed with one-click release
  • Rate-limit / block state with one-click unblock
  • Recordings library with in-browser asciinema replay + event seek
  • Grant approval form for ad-hoc time-limited users

Do not bind the dashboard to 0.0.0.0 on a publicly reachable host. Use a Tailscale tunnel or SSH -L forward.

6. Geofencing

Only allow logins from specific countries:

sudo codeguard geofence allow US,GB,DE,GE

Block specific countries:

sudo codeguard geofence deny CN,RU,KP

Country lookup uses ip-api.com, cached at /var/lib/codeguard/geo-cache.json for 24 h per IP. Falls open if the lookup itself fails — you won't be locked out by a temporary API outage. Loopback and Tailscale CGNAT (100.64.0.0/10) are exempt.

7. Backup before maintenance

sudo codeguard backup --out /root/codeguard-2026-05-26.tgz.enc

Encrypted snapshot of /etc/codeguard/, /var/lib/codeguard/, the profile.d hook, fail2ban filter + jail, the three systemd units, and the sudoers rule. Restore on a fresh host:

sudo codeguard restore /root/codeguard-2026-05-26.tgz.enc

Prompts for the passphrase, verifies archive integrity, prompts for an explicit yes before overwriting existing state.

Configuration files

Path Mode Purpose
/etc/codeguard/config.json 0600 Settings + secret hash. Hand-editable JSON.
/etc/codeguard/env 0400 Credentials referenced via ${VAR} from config.json.
/etc/codeguard/users/*.json 0600 Per-user mode entries.
/etc/codeguard/grants.json 0600 Active JIT grants.
/etc/codeguard/webauthn-credentials.json 0600 Registered passkeys.
/etc/sudoers.d/codeguard-grant 0440 Sudoers rule for grant users (auto-managed).
/etc/profile.d/codeguard.sh 0755 Login hook. Remove this file to disable CodeGuard instantly.
/etc/fail2ban/filter.d/codeguard.conf 0644 fail2ban filter (verify FAIL + sftp-gate DENY + exec-gate DENY).
/etc/fail2ban/jail.d/codeguard.conf 0644 fail2ban jail (3 fails / 10 min → 24 h ban).
/etc/systemd/system/codeguard-bot.service 0644 Telegram long-poller.
/etc/systemd/system/codeguard-dashboard.service 0644 Web UI.
/etc/systemd/system/codeguard-grant-tick.timer 0644 Per-minute grant expiry sweep.

Logs and state

Path Mode Purpose
/var/log/codeguard.log 0640 Plain-text audit log (chattr +a append-only).
/var/log/codeguard.jsonl 0640 JSON Lines audit log (SIEM-friendly).
/var/log/codeguard-honeypot.log 0640 Honeypot session transcripts.
/var/log/codeguard-sessions/*.cast 0600 Asciinema session recordings.
/var/log/codeguard-sessions/*.events.json 0600 Per-recording flagged events.
/var/lib/codeguard/ratelimit.json 0600 Per-IP rate-limit / cooldown / block state.
/var/lib/codeguard/honeypot.json 0600 Honeypot IP list (auto + manual).
/var/lib/codeguard/geo-cache.json 0600 GeoIP lookup cache.
/run/codeguard-approvals/ 0700 Per-token approval marker files (tmpfs).
/run/codeguard-kicks/ 0700 Live honeypot-kick flags (tmpfs).

Failure modes — when this breaks

CodeGuard is honest about its dependencies:

  • Telegram unreachable (region-blocked, bot suspended, network outage): approval flow degrades to "deny with cooldown" so attackers cannot exploit the outage. You can still log in interactively with the secret + recovery code.
  • You forgot the secret: 10 one-time recovery codes were printed during setup. If those are also lost, see the recovery section in docs/details.md.
  • Tailscale SSH bypasses CodeGuard entirely. Tailscale's --ssh feature uses its own SSH server inside tailscaled and does not pass through OpenSSH. Disable with tailscale set --ssh=false.
  • Ubuntu 24.04 user-namespace lockdown breaks bubblewrap jails. The installer prompts; codeguard grant ... --path warns and falls back to a soft fence if the kernel knob is not set.
  • The /usr/bin directory is visible inside grant jails. Read-only mount; tightening this to a per-mode allowlist is roadmap item v1.3.

Roadmap

v1.0 — All features described above are shipped: layered auth, Telegram approval, honeypot, recording, JIT grants with bubblewrap, encrypted backup, geofencing, WebAuthn, web dashboard, fail2ban integration.

Next:

  • v1.1 — Multi-admin approval (K-of-N admins to unlock)
  • v1.2 — Anomaly heuristics (suspicious command-sequence detection in honeypot + recordings)
  • v1.3--path --strict with a custom /usr/bin allowlist inside the jail
  • v2.0 — Federation: one Telegram bot approving across a fleet of hosts

Contributions welcome. Bug reports including codeguard doctor output get priority.

Security disclosure

Found a vulnerability? Please email george@orcca.cloud instead of filing a public issue. Full policy: SECURITY.md.

Detailed documentation

The per-feature deep dives live in docs/details.md:

  • SFTP gate — out-of-band approval workflow
  • Exec gate — closing the ssh host 'cmd' bypass
  • Anti-spam — rate limit, deny cooldown, block-by-tap
  • Honeypot — the fake shell and its alert catalogue
  • Session recording — asciinema integration and event extraction
  • JIT grants — bubblewrap jail internals, mode definitions, admin
  • Alerts — full transport configuration reference
  • Files — every path CodeGuard touches and why
  • Recovery — three independent ways back in
  • Known limitations — the full caveat list

License

MIT — see LICENSE.

About

Modern SSH security for self-hosted Linux — Telegram-approved access, honeypot diversion, session recording, JIT user grants. One Python file.

Topics

Resources

License

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors