sks5 0.0.2

Lightweight SSH server with SOCKS5 proxy, shell emulation, and ACL
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
# sks5

[![CI](https://github.com/galti3r/sks5/actions/workflows/ci.yml/badge.svg)](https://github.com/galti3r/sks5/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/galti3r/sks5/graph/badge.svg)](https://codecov.io/gh/galti3r/sks5) [![deps.rs](https://deps.rs/repo/github/galti3r/sks5/status.svg)](https://deps.rs/repo/github/galti3r/sks5) [![Crates.io](https://img.shields.io/crates/v/sks5.svg)](https://crates.io/crates/sks5) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![docs.rs](https://img.shields.io/docsrs/sks5)](https://docs.rs/sks5) [![Docker](https://img.shields.io/docker/v/dockerhubgalti3r/sks5?label=docker)](https://hub.docker.com/r/dockerhubgalti3r/sks5) [![Homebrew](https://img.shields.io/badge/homebrew-sks5-FBB040?logo=homebrew)](https://github.com/galti3r/homebrew-sks5) [![Release](https://img.shields.io/github/v/release/galti3r/sks5)](https://github.com/galti3r/sks5/releases/latest) [![MSRV](https://img.shields.io/badge/MSRV-1.88-orange.svg)](https://blog.rust-lang.org/)

> Zero-dependency scratch-based Docker images. Rootless by default. Single static binary. Multi-user auth. Real-time dashboard.

<p align="center">
  <img src="https://galti3r.github.io/sks5/demo.gif" alt="sks5 demo" width="800">
</p>

```mermaid
flowchart LR
    Client["SSH Client<br/><code>ssh -D / -L</code>"]
    SOCKS["SOCKS5 Client"]
    Browser["Browser / curl"]

    Client --> SSH
    SOCKS --> SOCKS5
    Browser --> REST

    subgraph sks5["sks5 Server"]
        direction TB
        SSH["SSH :2222"]
        SOCKS5["SOCKS5 :1080"]
        REST["REST API + Dashboard :8080"]

        subgraph pipeline["Request Pipeline"]
            direction LR
            RateLimit["Rate Limit<br/>+ Ban Check"]
            Auth["Auth Engine<br/>Argon2 / PubKey / TOTP"]
            ACL["ACL Filter<br/>CIDR · FQDN · GeoIP"]
            Quota["Quota Guard<br/>BW + Conn Limits"]
        end

        SSH --> RateLimit
        SOCKS5 --> RateLimit
        RateLimit --> Auth
        Auth --> ACL
        ACL --> Quota

        Proxy["Proxy Engine<br/>Pool + Retry"]
        Quota --> Proxy

        Shell["Shell Emulator"]
        Auth -.->|shell session| Shell

        Metrics["Prometheus Metrics"]
        Audit["Audit Log + Webhooks"]
        Proxy -.-> Metrics
        Proxy -.-> Audit
    end

    Proxy --> T1["Target A"]
    Proxy --> T2["Target B"]
    Proxy --> TN["Target N"]
```

## Dashboard

| Dark Theme | Light Theme |
|:---:|:---:|
| ![Dark](https://galti3r.github.io/sks5/screenshots/dashboard-dark.png) | ![Light](https://galti3r.github.io/sks5/screenshots/dashboard-light.png) |

## Feature Highlights

| Category | Features |
|----------|----------|
| :satellite: SSH & Proxy | SSH dynamic forwarding (`-D`), local forwarding (`-L`), standalone SOCKS5, TLS SOCKS5, connection pooling, smart retry with exponential backoff |
| :lock: Authentication | Argon2id passwords, SSH public keys, SSH certificates, TOTP 2FA, auth method chaining |
| :shield: Access Control | Per-user and global ACL, CIDR/FQDN wildcards, GeoIP country filtering, anti-SSRF guard, time-based access windows, account expiration |
| :no_entry: Security | Auto-ban (fail2ban-style), IP reputation scoring, pre-auth rate limiting, SFTP/SCP blocked, password zeroization, webhook SSRF protection |
| :computer: Shell | Virtual filesystem, `show` commands (status, bandwidth, connections, ACL, fingerprint, history), `test`/`ping`/`resolve`, bookmarks, aliases, tab completion, MOTD |
| :bar_chart: Observability | Prometheus metrics, structured audit logs, webhooks with HMAC, alerting rules, real-time web dashboard with SSE |
| :wrench: Management | REST API, hot config reload, maintenance mode/windows, user groups and roles, quotas (bandwidth + connections), broadcast/kick |
| :whale: Container | `scratch` image (~5 MB), Alpine variant (~12 MB), rootless (UID 65534), static musl binary, multi-arch (amd64/arm64) |
| :rocket: Deployment | Docker, Podman, Kubernetes, bare metal, single static binary, `cargo install` |

## Install

### Docker / Podman (recommended)

sks5 ships as a **~5 MB scratch image** — no OS, no shell, just the binary + CA certs.

```bash
# Docker
docker run -p 2222:2222 ghcr.io/galti3r/sks5:latest quick-start --password demo

# Podman (rootless)
podman run -p 2222:2222 ghcr.io/galti3r/sks5:latest quick-start --password demo
```

| Tag | Base | Size | Shell | Use case |
|-----|------|------|-------|----------|
| `latest` / `1.x.x` | scratch | ~5 MB | No | Production (minimal attack surface) |
| `latest-alpine` / `1.x.x-alpine` | Alpine 3.21 | ~12 MB | Yes | Debug / troubleshooting |

### Cargo

```bash
cargo install sks5
```

### Homebrew (macOS/Linux)

```bash
brew tap galti3r/sks5
brew install sks5
```

### Binary (GitHub Releases)

Download pre-built static binaries from [Releases](https://github.com/galti3r/sks5/releases/latest):
- `sks5-x86_64-linux-gnu.tar.gz`
- `sks5-x86_64-linux-musl.tar.gz` (static)
- `sks5-aarch64-linux-gnu.tar.gz`
- `sks5-aarch64-linux-musl.tar.gz` (static)

## Quick Start

### Zero config

```bash
# Auto-generated password
sks5 quick-start

# With a chosen password
sks5 quick-start --password demo

# With standalone SOCKS5 listener
sks5 quick-start --password demo --socks5-listen 0.0.0.0:1080

# Save generated config for later
sks5 quick-start --password demo --save-config config.toml
```

### Generate a config file

```bash
# Interactive (prompts for password)
sks5 init

# Non-interactive
sks5 init --username alice --password secret --output config.toml
```

### Manual setup

```bash
make build
cp config.example.toml config.toml
sks5 hash-password --password "your-password"
# Edit config.toml with the generated hash
make run
```

### Connect

```bash
# SSH shell
ssh -o StrictHostKeyChecking=no alice@localhost -p 2222

# Dynamic forwarding (SOCKS5 via SSH)
ssh -D 1080 -N alice@localhost -p 2222
curl --socks5 localhost:1080 http://example.com

# Local port forwarding
ssh -L 8080:httpbin.org:80 -N alice@localhost -p 2222
curl http://localhost:8080/ip

# Standalone SOCKS5
curl --socks5 alice:password@localhost:1080 http://example.com
```

## Configuration

See [config.example.toml](config.example.toml) for a complete configuration reference.

### Key sections

| Section | Required | Description |
|---------|:--------:|-------------|
| `[server]` | Yes | SSH/SOCKS5 listen addresses, host key, TLS, DNS cache, retry |
| `[[users]]` | Yes | User accounts (at least one required) |
| `[shell]` | No | Hostname, prompt, colors, autocomplete |
| `[limits]` | No | Connection limits, timeouts, idle warning |
| `[security]` | No | Auto-ban, IP guard, TOTP, IP reputation, CA keys |
| `[logging]` | No | Log level, format, audit log, flow logs |
| `[metrics]` | No | Prometheus endpoint |
| `[api]` | No | REST API listen address, bearer token |
| `[geoip]` | No | GeoIP country filtering |
| `[motd]` | No | Message of the Day template and colors |
| `[acl]` | No | Global ACL rules (inherited by all users) |
| `[[groups]]` | No | User groups for config inheritance |
| `[[webhooks]]` | No | HTTP webhooks with retry |
| `[alerting]` | No | Alert rules on bandwidth/connections/auth |
| `[[maintenance_windows]]` | No | Scheduled maintenance windows |
| `[connection_pool]` | No | TCP connection pooling |
| `[upstream_proxy]` | No | Upstream SOCKS5 proxy |

### User groups

Groups allow shared configuration inheritance. Users reference groups by name:

```toml
[[groups]]
name = "developers"
max_bandwidth_kbps = 10240  # 10 Mbps
allow_forwarding = true
allow_shell = true

[groups.acl]
default_policy = "allow"
deny = ["10.0.0.0/8:*"]

[[users]]
username = "alice"
password_hash = "$argon2id$..."
group = "developers"  # Inherits group settings
```

Inheritance order: **user > group > global defaults**. User fields override group, group overrides global.

### MOTD templates

The MOTD supports variable substitution and ANSI colors:

```toml
[motd]
enabled = true
colors = true
template = """
Welcome {user}! Connected from {source_ip}
Role: {role} | Group: {group}
Bandwidth: {bandwidth_used}/{bandwidth_limit}
Server uptime: {uptime}
"""
```

Available variables: `{user}`, `{auth_method}`, `{source_ip}`, `{connections}`, `{acl_policy}`, `{denied}`, `{expires_at}`, `{bandwidth_used}`, `{bandwidth_limit}`, `{last_login}`, `{uptime}`, `{version}`, `{group}`, `{role}`.

Per-user/group MOTD overrides the global one.

<details>
<summary><strong>Shell commands</strong></summary>

| Command | Description | Permission |
|---------|-------------|------------|
| `ls` / `cd` / `pwd` / `cat` | Virtual filesystem navigation | Always |
| `whoami` / `id` / `hostname` | Identity commands | Always |
| `echo` / `env` / `uname` | Basic utilities | Always |
| `help` / `exit` / `clear` | Shell control | Always |
| `show connections` | Live active proxy connections count | `show_connections` |
| `show bandwidth` | Live bandwidth usage (daily/monthly/hourly/rate) | `show_bandwidth` |
| `show acl` | ACL rules for current user | `show_acl` |
| `show status` | Session info with live connection count | `show_status` |
| `show history` | Connection summary (today/this month) | `show_history` |
| `show fingerprint` | SSH key fingerprint (from auth) | `show_fingerprint` |
| `test host:port` | TCP connectivity test | `test_command` |
| `ping host` | Simulated ICMP ping | `ping_command` |
| `resolve hostname` | DNS lookup | `resolve_command` |
| `bookmark add/list/del` | Manage host:port bookmarks | `bookmark_command` |
| `alias add/list/del` | Command aliases | `alias_command` |

Permissions are configured via `shell_permissions` at user, group, or global level.

</details>

### Quotas, rate limiting, and time-based access

```toml
[[users]]
username = "contractor"
password_hash = "$argon2id$..."
group = "external"
expires_at = "2026-06-30T23:59:59Z"
max_connections = 5

[users.rate_limits]
connections_per_second = 2
connections_per_minute = 30
connections_per_hour = 200

[users.quotas]
daily_bandwidth_bytes = 1073741824    # 1 GB/day
monthly_connection_limit = 1000
bandwidth_per_hour_bytes = 536870912  # 512 MB/hour rolling

[users.time_access]
access_hours = "08:00-18:00"
access_days = ["mon", "tue", "wed", "thu", "fri"]
timezone = "Europe/Paris"
```

Server-level rate limiting in `[limits]`:

```toml
[limits]
max_bandwidth_mbps = 100
max_new_connections_per_second = 50
max_new_connections_per_minute = 500
```

### Alerting

```toml
[alerting]
enabled = true

[[alerting.rules]]
name = "high_bandwidth"
condition = "bandwidth_exceeded"
threshold = 1073741824       # 1 GB
window_secs = 3600           # per hour
webhook_url = "https://hooks.example.com/alert"

[[alerting.rules]]
name = "brute_force"
condition = "auth_failures"
threshold = 50
window_secs = 300
users = []                   # all users
```

### Connection pooling and smart retry

```toml
[connection_pool]
enabled = true
max_idle_per_host = 10
idle_timeout_secs = 60

[server]
connect_retry = 3            # retry 3 times on failure
connect_retry_delay_ms = 500 # initial delay 500ms, doubles each retry, max 10s
```

<details>
<summary><strong>Environment variables</strong></summary>

sks5 supports full configuration via environment variables for Docker/Kubernetes deployments. See `docker-compose.yml` for complete examples.

| Variable | Description | Default |
|----------|-------------|---------|
| `SKS5_CONFIG` | Path to config file | -- |
| `SKS5_SSH_LISTEN` | SSH listen address | `0.0.0.0:2222` |
| `SKS5_SOCKS5_LISTEN` | Standalone SOCKS5 listen address | (disabled) |
| `SKS5_HOST_KEY_PATH` | SSH host key path | `host_key` |
| `SKS5_LOG_LEVEL` | Log level (trace/debug/info/warn/error) | `info` |
| `SKS5_LOG_FORMAT` | Log format (pretty/json) | `pretty` |
| `SKS5_SHUTDOWN_TIMEOUT` | Graceful shutdown timeout (seconds) | `30` |
| `SKS5_DNS_CACHE_TTL` | DNS cache TTL (-1=native, 0=off, N=seconds) | `-1` |
| `SKS5_METRICS_ENABLED` | Enable Prometheus metrics | `false` |
| `SKS5_API_ENABLED` | Enable management API | `false` |
| `SKS5_API_TOKEN` | API bearer token | -- |
| `SKS5_BAN_ENABLED` | Enable auto-ban | `true` |

**Multi-user mode** (indexed):

| Variable | Description |
|----------|-------------|
| `SKS5_USER_<N>_USERNAME` | Username (N = 0, 1, 2, ...) |
| `SKSKS5_USER_<N>_PASSWORD_HASH` | Argon2id password hash |
| `SKS5_USER_<N>_ALLOW_FORWARDING` | Allow forwarding (true/false) |
| `SKS5_USER_<N>_ALLOW_SHELL` | Allow shell access (true/false) |
| `SKS5_USER_<N>_TOTP_ENABLED` | Enable TOTP 2FA |
| `SKSKS5_USER_<N>_TOTP_SECRET` | Base32-encoded TOTP secret |
| `SKS5_USER_<N>_MAX_BANDWIDTH_KBPS` | Per-connection bandwidth cap |
| `SKS5_USER_<N>_MAX_AGGREGATE_BANDWIDTH_KBPS` | Total bandwidth cap |

**Docker/K8s secrets** (`_FILE` convention): append `_FILE` to read from a file path instead of the env var value directly.

| Variable | Supports `_FILE` |
|----------|:-:|
| `SKS5_PASSWORD_HASH` / `SKS5_USER_<N>_PASSWORD_HASH` | Yes |
| `SKS5_API_TOKEN` | Yes |
| `SKS5_TOTP_SECRET` / `SKS5_USER_<N>_TOTP_SECRET` | Yes |

</details>

## API

The HTTP API is protected by Bearer token (configured in `api.token`).

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/health` | Health check (no auth) |
| GET | `/api/status` | Server status (uptime, connections, users) |
| GET | `/api/users` | List users (no password hashes) |
| GET | `/api/users?details=true` | Extended user info with connection stats |
| GET | `/api/connections` | Active connections per user |
| GET | `/api/bans` | Banned IPs |
| DELETE | `/api/bans/:ip` | Unban an IP |
| POST | `/api/maintenance` | Toggle maintenance mode |
| POST | `/api/reload` | Hot-reload config from disk |
| POST | `/api/sse-ticket` | Generate short-lived HMAC ticket for SSE auth |
| POST | `/api/kick/:username` | Disconnect all sessions for a user |
| POST | `/api/broadcast` | Broadcast message to all connected users |
| GET | `/api/quotas` | List all users' quota usage |
| GET | `/api/quotas/:username` | Quota usage detail for a specific user |
| POST | `/api/quotas/:username/reset` | Reset quota counters for a user |
| GET | `/api/ssh-config` | Generate SSH config snippet (`?user=&host=`) |
| GET | `/api/events` | SSE stream (token via `?token=` or `?ticket=`) |
| GET | `/dashboard` | Web dashboard (no auth required) |

## Health Check

```bash
sks5 health-check --addr 127.0.0.1:2222 --timeout 5
```

Returns exit code 0 if the SSH listener is accepting connections, 1 otherwise. Used in Docker healthchecks and monitoring.

## Container

sks5 images are available in two variants, both multi-arch (amd64 + arm64):

| Variant | Image | Base | Size | Rootless |
|---------|-------|------|------|:--------:|
| **Minimal** (default) | `ghcr.io/galti3r/sks5:latest` | `scratch` | ~5 MB | UID 65534 |
| **Alpine** | `ghcr.io/galti3r/sks5:latest-alpine` | Alpine 3.21 | ~12 MB | UID 1000 |

### Quick Start with Docker / Podman

```bash
# Minimal (scratch) — production
docker run -p 2222:2222 -p 1080:1080 \
  -v ./config.toml:/etc/sks5/config.toml:ro \
  ghcr.io/galti3r/sks5:latest

# Alpine — with shell for debugging
docker run -it -p 2222:2222 \
  ghcr.io/galti3r/sks5:latest-alpine sh

# Podman (drop-in replacement)
podman run -p 2222:2222 ghcr.io/galti3r/sks5:latest quick-start --password demo
```

> **Podman users**: Replace `docker` with `podman` in all commands.
> sks5 is fully compatible with rootless Podman.

### Build Locally

```bash
make docker-build          # Build scratch image (sks5:latest)
make docker-build-alpine   # Build Alpine image (sks5:alpine)
make docker-build-all      # Build both variants
make docker-run            # Run scratch container
make docker-run-alpine     # Run Alpine container
```

### Compose (Docker & Podman)

```bash
docker compose up -d                # or: podman-compose up -d
docker compose --profile env up -d  # Environment variable mode
docker compose --profile tls up -d  # TLS SOCKS5 mode
```

### Multi-architecture builds

Build for both `linux/amd64` and `linux/arm64` (Apple Silicon, AWS Graviton, etc.).

**Cross-compilation (fast, recommended):**

```bash
make docker-build-cross
```

**QEMU emulation (slower, simpler):**

```bash
make docker-build-multiarch
```

**Push to registry:**

```bash
PUSH=true IMAGE_NAME=ghcr.io/user/sks5 IMAGE_TAG=v1.0.0 make docker-build-cross
```

### CI/CD

Multi-arch images are automatically built and pushed to GHCR on tag pushes via CI.

CI pipelines are provided for three platforms:

| Platform | Config | Pipeline |
|----------|--------|----------|
| **GitHub Actions** | `.github/workflows/ci.yml` | lint + test + coverage + security + SARIF + Docker |
| **GitLab CI** | `.gitlab-ci.yml` | lint + test + coverage + security + Docker + Pages |
| **Forgejo Actions** | `.forgejo/workflows/ci.yml` | lint + test + security + build + Docker |

All pipelines include:
- `cargo fmt --check` + `cargo clippy -D warnings` + `hadolint`
- `cargo test --all-targets`
- `cargo audit` + `cargo deny check`
- Multi-arch Docker build + push to registry
- Container vulnerability scanning (Trivy)

## Testing

```bash
make test              # All tests (unit + E2E)
make test-unit         # Unit tests only
make test-e2e          # E2E tests only
make test-e2e-all      # E2E including ignored (IPv6, perf)
make test-e2e-browser  # Browser E2E (Chrome Headless in Podman)
make test-perf         # Performance benchmarks (throughput, latency, concurrency)
make security-scan     # Security scan (clippy + cargo-audit + cargo-deny)
make test-all          # Full suite (tests + security)
make test-e2e-podman   # E2E in Podman containers (isolated network)
make bench             # Criterion benchmarks (ACL, password, config, SOCKS5)
```

### Test coverage

| Category | Tests | Description |
|----------|------:|-------------|
| Unit (lib) | 387 | Config, ACL, auth, shell, SOCKS5 protocol, proxy, security, show commands, session, alerting, quotas, groups, roles, connection pool, MOTD, IP reputation |
| Unit (standalone) | 941 | Auth service, CLI, config validation, connectors, geoip, webhooks, pre-auth, source IP, audit, protocol fuzz, SSH keys, pubkey, DNS cache, metrics, SSE ticket, IP rate limiter, SOCKS5 TLS, webhook retry, API users, SOCKS5 timeout, proxy details, smart retry, time access |
| E2E - Auth | 5 | Password success/failure, unknown user, shell prompt, retry |
| E2E - Shell | 16 | Exec commands, dangerous commands blocked, interactive shell |
| E2E - Shell commands | 18 | show status/bandwidth/connections, help, echo, alias, unknown commands |
| E2E - ACL | 12 | FQDN, subnet/CIDR, combined rules, wildcard, IPv6 |
| E2E - Forwarding | 3 | Local forward, large data, denied user |
| E2E - Rejection | 8 | SFTP, reverse forward, bash/sh/nc/rsync blocked |
| E2E - SOCKS5 | 18 | Auth, forwarding, concurrency, anti-SSRF, standalone, timeout |
| E2E - API | 39 | Health, users, connections, bans, maintenance, auth, dashboard, SSE, groups, sessions, audit |
| E2E - Quota API | 10 | List quotas, user detail, reset, auth, enforcement, rate limiting, Prometheus metrics |
| E2E - Reload | 3 | Valid/invalid config reload, auth required |
| E2E - Status | 3 | Health, Prometheus metrics, maintenance mode |
| E2E - Autoban | 3 | Trigger after threshold, reject banned IP, no false positive |
| E2E - Audit | 2 | Auth success/failure audit events |
| E2E - Webhooks | 11 | Webhook delivery, retry logic, HMAC signatures |
| E2E - Performance | 5 | Throughput, latency, concurrent connections |
| E2E - SSH sessions | 3 | Session tracking and management |
| E2E - SSE/WS | 12 | SSE ticket auth, SSE payloads, WebSocket connections |
| E2E - Backup/Restore | 6 | Config backup and restore via API |
| E2E - CLI | 10 | CLI subcommands, completions, help output |
| E2E - Browser | 9 | Dashboard E2E with Chrome Headless (Podman) |
| **Total** | **~940+** | **1517 test functions across all binaries** |

## Security

### Hardening features

- Argon2id password hashing, SSH public key auth, SSH certificate auth
- TOTP 2FA (Google Authenticator / Authy compatible)
- Auth method chaining (e.g. require pubkey + password)
- Per-user ACL (CIDR, FQDN wildcard, port ranges)
- Auto-ban (fail2ban-style), global/per-user IP whitelist
- IP reputation scoring (dynamic, exponential decay)
- Per-IP pre-auth rate limiting (`max_new_connections_per_ip_per_minute`)
- Multi-window rate limiting (per-second/minute/hour, per-user and server-level)
- GeoIP country filtering
- Anti-SSRF IP guard (blocks RFC 1918, link-local, loopback, multicast)
- Webhook SSRF protection (private IP blocking + DNS rebinding guard)
- SOCKS5 password zeroization (secure memory handling)
- Pre-auth ban check (reject before auth attempt)
- Metrics cardinality protection (label cap prevents high-cardinality explosion)
- SSE ticket auth (HMAC-based short-lived tickets, no API token in URL)
- Virtual filesystem (zero real file exposure)
- SFTP/SCP/reverse forwarding blocked
- Time-based access control (per-user login hours/days)
- Account expiration with automatic denial
- Scratch-based container (~5 MB, zero packages, no shell)
- Rootless container (runs as non-root UID)
- Static musl binary (no dynamic library dependencies)
- Multi-arch images signed with cosign (Sigstore keyless)
- SLSA provenance attestation via GitHub Actions
- CycloneDX SBOM included in releases

### Security scanning

```bash
# Run all security checks
make security-scan

# Individual tools
cargo clippy --all-targets -- -D warnings
cargo audit
cargo deny check
```

Configuration: [`deny.toml`](deny.toml) (licenses, advisories, sources), [`.hadolint.yaml`](.hadolint.yaml) (Dockerfile lint).

### Audit report

See [docs/SECURITY-AUDIT.md](docs/SECURITY-AUDIT.md) for the full code security audit.

## Architecture

See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).

## License

MIT