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.
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.
git clone https://github.com/GeorgeBigh/CodeGuard.git
cd CodeGuard
sudo ./install.shThe 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.
sudo codeguard setupInteractive flow:
- Choose UI language — English or Georgian (ქართული).
- Set the prompt timeout (default 30 s).
- 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. - 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. - 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.
sudo codeguard doctorRuns 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.shdisables the gate instantly without uninstalling CodeGuard.
┌─────────────┐ ┌─────────────┐ ┌────────────────────────────┐
│ ssh client │ ─► │ sshd (auth) │ ─► │ /etc/profile.d/codeguard.sh│
└─────────────┘ └─────────────┘ │ → codeguard verify │
└────────────┬───────────────┘
│ ok
▼
exec $SHELL
- OpenSSH authenticates with the user's normal password or SSH key.
- Before the user's login shell starts,
/etc/profile.d/codeguard.shinvokescodeguard 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. - 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.
The CLI is a single binary; all subcommands are documented below.
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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). |
| 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. |
| Command | Purpose |
|---|---|
codeguard dashboard [PORT] [--bind ADDR] |
Start the single-page web UI. Default 127.0.0.1:8088. |
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:
Test:
sudo codeguard alerts-test telegramThe message should arrive on Telegram with all placeholders substituted.
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.
sudo codeguard recording onEvery 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 50A contractor needs four hours of read-only access to /var/www/html:
sudo codeguard grant alice --hours 4 --mode readonly --path /var/www/htmlWhat happens:
- A Linux user
cg_aliceis created with a one-time random password (printed once on stdout — hand off via Signal/Telegram). - The user is added to the
cg_grantsgroup, which a tight/etc/sudoers.d/codeguard-grantrule references (no wildcards, no privilege escalation). - A per-grant shell wrapper is generated under
/usr/local/lib/codeguard/shells/. The wrapper restricts the PATH to the mode allowlist. - If bubblewrap is installed, the shell launches in a per-session
namespace jail: only
/data(a bind-mount of--path) is writable,/usr/binis read-only,/etc/passwdand/etc/groupare synthetic with just root +cg_alice, andno_new_privsis set sosudoand setuid binaries are rejected by the kernel. - A systemd timer (
codeguard-grant-tick.timer) sweeps every minute and runsuserdel -rplus killing live sessions when the grant expires. - Telegram pings on create, on login, and on expire / revoke. The
login alert carries a
🔚 Revoke nowinline 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 activesudo systemctl enable --now codeguard-dashboardDefault bind: 127.0.0.1:8088. From your laptop:
ssh -L 8088:127.0.0.1:8088 root@your-hostOpen 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.0on a publicly reachable host. Use a Tailscale tunnel or SSH-Lforward.
Only allow logins from specific countries:
sudo codeguard geofence allow US,GB,DE,GEBlock specific countries:
sudo codeguard geofence deny CN,RU,KPCountry 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.
sudo codeguard backup --out /root/codeguard-2026-05-26.tgz.encEncrypted 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.encPrompts for the passphrase, verifies archive integrity, prompts for
an explicit yes before overwriting existing state.
| 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. |
| 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). |
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 indocs/details.md. - Tailscale SSH bypasses CodeGuard entirely. Tailscale's
--sshfeature uses its own SSH server insidetailscaledand does not pass through OpenSSH. Disable withtailscale set --ssh=false. - Ubuntu 24.04 user-namespace lockdown breaks bubblewrap jails.
The installer prompts;
codeguard grant ... --pathwarns and falls back to a soft fence if the kernel knob is not set. - The
/usr/bindirectory is visible inside grant jails. Read-only mount; tightening this to a per-mode allowlist is roadmap item v1.3.
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 --strictwith a custom/usr/binallowlist 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.
Found a vulnerability? Please email george@orcca.cloud instead of
filing a public issue. Full policy: SECURITY.md.
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
MIT — see LICENSE.