Smooth Operator is a cloud-native, clientless remote access vault. It gives teams secure, browser-based access to SSH, RDP, and VNC servers—without VPN clients, exposed ports, or direct credential sharing.
End users never see actual credentials. Admins control exactly who can access what, through granular role-based permissions and vault assignments.
Full documentation → smooth-operator-docs.pages.dev (or run locally with
docker compose up docs)
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#ffffff',
'primaryBorderColor': '#000000',
'primaryTextColor': '#000000',
'lineColor': '#000000',
'fontSize': '16px',
'fontFamily': 'Inter, system-ui, sans-serif',
'mainBkg': '#ffffff',
'edgeLabelBackground': '#ffffff',
'clusterBkg': '#f8f9fa',
'clusterBorder': '#000000'
}
}}%%
graph TD
Browser([Browser]) -->|HTTPS + WebSocket| Frontend
subgraph Docker Stack
Frontend["Angular 21\nnginx · :4200"]
Backend[".NET 10 API\n:5000"]
DB[("PostgreSQL 18\n:5432")]
Cache[("Redis 7\n:6379")]
Guacd["guacd 1.6\nApache Guacamole\n:4822"]
Docs["Docusaurus\nDocs Site · :3000"]
subgraph Observability
Loki["Loki (Logs)"]
Prom["Prometheus (Metrics)"]
Tempo["Tempo (Traces)"]
end
end
Frontend -->|REST + WSS| Backend
Backend -->|EF Core / Npgsql| DB
Backend -->|StackExchange.Redis| Cache
Backend -->|TCP 4822| Guacd
Backend -->|OTLP| Tempo
Backend -->|Metrics| Prom
Backend -->|Logs| Loki
Guacd -->|SSH / RDP / VNC| Targets[Target Servers]
| Component | Technology | Role |
|---|---|---|
| Frontend | Angular 21, Tailwind CSS 4, guacamole-common-js | SPA served via nginx; lazy-loaded feature modules, NgRx Signal Stores, runtime config |
| Backend | .NET 10, ASP.NET Core, EF Core 10 | Clean Architecture (Domain/Application/Infrastructure/Api); CQRS via MediatR; REST API + WebSocket tunnel |
| Database | PostgreSQL 18.4 | Users, vaults, connections, credentials, groups, audit logs |
| Cache | Redis 7 | Rate limiting, session state |
| Connection Engine | Apache guacd 1.6 | Translates RDP/SSH/VNC to Guacamole protocol over WebSocket |
| Observability | Prometheus, Loki, Tempo | Metrics, centralized logging, and distributed tracing |
| Docs | Docusaurus 3 | User guide, admin guide, and API reference |
backend/
├── src/
│ ├── SmoothOperator.Domain/ # Entities, value objects. Zero dependencies.
│ ├── SmoothOperator.Application/ # MediatR commands/queries/handlers, FluentValidation,
│ │ # DTOs, Options classes, ports (interfaces).
│ ├── SmoothOperator.Infrastructure/ # EF Core, Redis, MailKit, encryption, SSO adapters.
│ └── SmoothOperator.Api/ # Thin controllers (single MediatR.Send per action),
│ # DI composition root, middleware, Program.cs.
└── tests/
├── SmoothOperator.Api.Tests/ # WebApplicationFactory integration tests.
└── SmoothOperator.ArchitectureTests/ # NetArchTest layer-boundary enforcement.
Dependency rule: Api → Application → Domain; Infrastructure → Application, Domain.
%%{init: {
'theme': 'base',
'themeVariables': {
'actorBkg': '#ffffff',
'actorBorder': '#000000',
'actorTextColor': '#000000',
'actorLineColor': '#000000',
'signalColor': '#000000',
'signalTextColor': '#000000',
'labelBoxBkgColor': '#ffffff',
'labelBoxBorderColor': '#000000',
'labelTextColor': '#000000',
'loopTextColor': '#000000',
'noteBkgColor': '#ffffff',
'noteBorderColor': '#000000',
'noteTextColor': '#000000',
'mainBkg': '#ffffff',
'fontSize': '16px',
'fontFamily': 'Inter, system-ui, sans-serif'
}
}}%%
sequenceDiagram
actor User as User
participant FE as Angular Frontend
participant API as .NET API
participant DB as PostgreSQL
participant G as guacd
participant Srv as Target Server
User->>FE: Click "Connect"
FE->>API: POST /api/guacamole/ticket/{id}
API->>DB: Check permission + fetch params
DB-->>API: OK + encrypted credentials
API-->>FE: One-time ticket token
FE->>API: WS /api/guacamole/connect/{id}?ticket=…
API->>G: Guacamole handshake (host, port, creds)
G->>Srv: SSH / RDP / VNC protocol
Srv-->>G: Remote desktop stream
G-->>API: Guacamole binary frames
API-->>FE: WebSocket relay
FE->>User: Live HTML5 Canvas session
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#ffffff',
'primaryBorderColor': '#000000',
'primaryTextColor': '#000000',
'lineColor': '#000000',
'fontSize': '16px',
'fontFamily': 'Inter, system-ui, sans-serif',
'mainBkg': '#ffffff',
'edgeLabelBackground': '#ffffff'
}
}}%%
erDiagram
USER {
uuid id
string email
string displayName
string role
bool isActive
}
USER_GROUP {
uuid id
string name
}
VAULT {
uuid id
string name
}
CONNECTION {
uuid id
string name
string protocol
string host
int port
}
CREDENTIAL {
uuid id
string name
string type
}
KNOWN_HOST {
uuid id
string hostname
string fingerprint
}
AUDIT_LOG {
uuid id
string action
string resourceType
timestamp createdAt
}
USER }o--o{ USER_GROUP : "member of"
USER_GROUP }o--o{ VAULT : "has access to"
USER }o--o{ VAULT : "directly assigned"
VAULT ||--o{ CONNECTION : "contains"
CONNECTION }o--o| CREDENTIAL : "uses"
CONNECTION }o--o| KNOWN_HOST : "validates"
USER ||--o{ AUDIT_LOG : "generates"
- Role-Based Access Control (RBAC) — four built-in roles:
Owner,Admin,TeamAdmin,User - Vault-based isolation — users only see connections in vaults they're assigned to, never raw credentials
- Invite-only registration — no public self-registration; admins send email invites
- Rate limiting — fixed-window limiter on auth endpoints (5 req/min per IP)
- Idle + max session timeouts — admins enforce both an inactivity cap and an absolute lifetime on every live Guacamole connection. Independent settings; either can be disabled.
- Local auth — username/password with BCrypt hashing + HS256 JWT
- TOTP two-factor authentication — RFC 6238 TOTP enrolment via QR code, one-time recovery codes (10 per user, downloadable as
.txt) - Personal Access Tokens (PATs) — long-lived bearer tokens (
sop_…) for CLI/CI/Terraform; SHA-256 hashed at rest, FixedTimeEquals verified, optional expiry, soft revoke. PATs cannot mint other PATs. - SSO via OIDC — plug in any OpenID Connect provider (Azure AD, Okta, Auth0, …)
- SSO via SAML 2.0 — enterprise identity federation
- Forgot-password flow — email-based password reset (requires SMTP)
- RDP, SSH, VNC — all three protocols via Apache Guacamole
- Browser-native — zero client software; runs on any modern browser
- HTML5 Canvas rendering — full keyboard/mouse capture via
guacamole-common-js - Clipboard sharing — bidirectional clipboard between browser and remote session
- SSH key pair generation — generate and store SSH keys directly in the vault
- User & group management — create groups, assign members, bulk-grant vault access
- Known hosts — store and verify SSH host fingerprints
- SMTP configuration — connect any SMTP server for invite/reset emails; test with one click
- Audit logs — complete action history with IP addresses, exportable to CSV; trigram-indexed for case-insensitive search at scale
- Credential vault — store passwords and SSH keys, encrypted at rest
- Liveness + readiness probes —
/health/live(process up) and/health/ready(DB + Redis reachable) for k8s/uptime monitors - Public status endpoint —
GET /status.jsonreturns build version, git SHA, uptime, and per-check status. No auth required, outside the rate limiter. - Prometheus metrics —
/metrics(bearer-token gated outside Dev/Testing). MediatR pipeline emits per-request duration histograms.
- Operator Glass design system — glassmorphic UI built on Material Design 3 color tokens
- Light / dark theme — auto-detects
prefers-color-scheme; user-toggleable at runtime - Fully responsive — works on desktop and tablets
- See frontend/DESIGN_SYSTEM.md for the full token reference
The "1.0" cut: 2FA, automation tokens, compliance-grade session enforcement, and the operational hooks every monitor expects.
- ✅ TOTP MFA — RFC 6238 enrolment + verification, 10 recovery codes per user, downloadable as
.txt - ✅ Personal Access Tokens —
sop_bearer tokens with SHA-256 hashing, optional expiry, soft revoke, profile-page UI - ✅ Idle + max session timeouts — enforced inside
GuacamoleProxyServicevia a watchdog task; closes WebSockets withPolicyViolationand auditssession.timeout_closed - ✅ Health endpoints split —
/health/live(liveness) +/health/ready(DB + Redis) + public/status.jsonwith version/uptime - ✅ Public docs site — Docusaurus deployed via Cloudflare Pages; in-app help links resolve to the public domain
- ✅ Production deploy pipeline — multi-arch Docker images, runtime
API_URL, Komodo TOML
| Version | Status | Headline | Notes |
|---|---|---|---|
| v1.0.1 | ✅ shipped | Webhooks for audit events | HMAC-signed Features/Webhooks/ hooked into AuditService. Trivial once PATs landed — same auth-token mental model. |
| v1.0.2 | ✅ shipped | Session recording + playback | GuacamoleProxyService passes recording-path + create-recording-path to guacd; playback page reuses Guacamole.SessionRecording from guacamole-common-js. The defining vault feature. |
| v1.0.3 | ✅ shipped | In-session file transfer | SFTP for SSH + drive redirect for RDP, gated by a FileTransferPolicy (vault default + per-connection override, disabled by default). Every transfer audits session.file_transferred. |
| v1.0.4 | ⬜ next | Backup & restore | Encrypted bundle (DB dump + Data Protection keys + system settings) plus dotnet smooth-operator backup/restore CLI surface. |
| v1.0.5 | ✅ shipped | First-run onboarding tour + empty states | Extended the existing empty-state.ts pattern to every list page; guided popover tour after SetupCommand succeeds. |
| — | ✅ shipped | i18n (pt-BR + en) | Pulled forward from v1.1+. @ngx-translate/core with public/i18n/{en,pt-BR}.json at full key parity and a runtime language switcher. |
- WebAuthn / Passkeys — pairs with TOTP (same auth flow extension via
Fido2NetLib). Deferred to keep the v1.0 surface focused on the recovery-codes UX. - JIT access workflow — high design cost; revisit when a customer pulls.
- SCIM 2.0 provisioning — only when an enterprise SSO customer asks.
- Terraform provider /
smopctlCLI — waits for stable API; PATs prep the ground.
Tracked separately so they don't get re-litigated:
- Compiled EF queries — gated on benchmark data
- Broad output-cache expansion — per-endpoint follow-up
- Redis-backed rate limiting — affects HA story; revisit when running >1 backend replica
- Docker Desktop (or Docker Engine + Compose)
- Ports
4200,5000,3000,5432,6379,4822available
git clone https://github.com/alessandrocaetanob/smooth-operator.git
cd smooth-operator
docker compose up --build| Service | URL |
|---|---|
| App | http://localhost:4200 |
| API | http://localhost:5000 |
| API Docs (Swagger) | http://localhost:5000/swagger |
| Docs site | http://localhost:3000 |
Open http://localhost:4200. On first run the app redirects to the setup wizard where you create the initial Owner account, set a display name, and (optionally) configure SMTP.
From Settings → Users, use the Invite button to send invite links to team members.
- Settings → Vaults — create a vault (e.g. "Production Linux")
- Connections — add an SSH/RDP/VNC connection, assign it to the vault
- Settings → Users — assign users (or groups) to the vault
Users see their assigned connections under My Access or My Vaults and can launch a live session with one click.
cd frontend
npm install
npm start
# → http://localhost:4200cd backend/src/SmoothOperator.Api
dotnet restore ../../../smooth-operator.sln
dotnet run
# → http://localhost:5000
# → Swagger UI at http://localhost:5000/swagger (Development only)cd docs
npm install
npm start
# → http://localhost:3000All variables follow the Options pattern — nested keys use __ as separator in environment variables.
See .env.example for a full template with generation hints.
| Variable | Service | Description |
|---|---|---|
ConnectionStrings__DefaultConnection |
backend | PostgreSQL connection string |
ConnectionStrings__Redis |
backend | Redis host:port |
Encryption__Key |
backend | 64-char hex (256-bit) — encrypts stored credentials. Generate: openssl rand -hex 32 |
Jwt__Key |
backend | ≥32-byte random secret for HS256 signing. Generate: openssl rand -hex 32 |
| Variable | Default | Description |
|---|---|---|
ASPNETCORE_ENVIRONMENT |
Production |
Development enables Swagger UI and detailed errors |
Cache__UseRedis |
false |
Back the ASP.NET output cache with Redis instead of in-memory — recommended when running multiple backend replicas |
Jwt__Issuer |
smooth-operator |
JWT issuer claim |
Jwt__Audience |
smooth-operator-api |
JWT audience claim |
Jwt__AccessTokenExpirationMinutes |
60 |
Access token lifetime |
Jwt__RefreshTokenExpirationDays |
7 |
Refresh token lifetime |
Guacd__Host |
guacd |
guacd service hostname |
Guacd__Port |
4822 |
guacd TCP port |
AppUrls__App |
http://localhost:4200 |
Public app URL (used in email links) |
AppUrls__Frontend |
http://localhost:4200 |
Frontend origin for CORS allow-list |
AppUrls__AllowedOrigins |
[] |
Comma-separated extra CORS origins |
DataProtection__KeysPath |
/data/protection-keys |
Where ASP.NET Data Protection writes key ring |
Otel__Endpoint |
(disabled) | OpenTelemetry OTLP gRPC endpoint |
Otel__ServiceName |
smooth-operator-backend |
Service name in traces/metrics |
| Variable | Description |
|---|---|
AzureAd__Instance |
https://login.microsoftonline.com/ — enables Entra ID SSO |
AzureAd__TenantId |
Entra ID tenant GUID |
AzureAd__ClientId |
Entra ID app client ID |
AzureAd__ClientSecret |
Entra ID client secret |
| Variable | Description |
|---|---|
Smtp__Host |
SMTP server hostname |
Smtp__Port |
SMTP port (default 587) |
Smtp__Username |
SMTP username |
Smtp__Password |
SMTP password |
Smtp__FromAddress |
From address for invite/reset emails |
| Variable | Default | Description |
|---|---|---|
APP_HELP_URL |
http://localhost:3000 |
Help link in sidebar |
APP_DOCS_URL |
http://localhost:3000 |
Docs link in login page |
APP_FEATURE_FLAGS |
{} |
JSON feature-flag map |
Security: The default
Encryption__KeyandJwt__Keyindocker-compose.ymlare placeholders for development only. Always supply strong random secrets in non-local environments — via Docker secrets, a secrets manager, or a.envfile that is never committed.
ASP.NET Core's Data Protection system encrypts session cookies and antiforgery tokens. By default the key ring is stored as plaintext XML in the dp_keys Docker volume. For production deployments you should encrypt the key ring at rest using one of the options below.
- Create or reuse an Azure Key Vault and add a Key (RSA 2048 or RSA-HSM 3072).
- Grant your backend's managed identity (or service principal) the
Key WrapandKey Unwrappermissions on that key. - Pass the full key identifier URI to the backend:
DataProtection__AzureKeyVaultKeyId=https://my-vault.vault.azure.net/keys/dp-key
Authentication uses DefaultAzureCredential — it auto-detects managed identity in Azure, or falls back to AZURE_CLIENT_ID / AZURE_CLIENT_SECRET / AZURE_TENANT_ID environment variables for service-principal auth in other environments.
Step 1 — Generate the certificate (one-time):
openssl req -x509 -newkey rsa:4096 -keyout dp.key -out dp.crt -days 3650 -nodes \
-subj "/CN=smooth-operator-dp/O=Your Company"
openssl pkcs12 -export -out dp.pfx -inkey dp.key -in dp.crt -passout pass:Store dp.pfx in a secure location (secrets manager or password vault). Never commit it to the repository.
Step 2 — Mount it as a Docker secret in docker-compose.override.yml:
secrets:
dp_cert:
file: ./secrets/dp.pfx
services:
backend:
secrets:
- dp_cert
environment:
- DataProtection__CertPath=/run/secrets/dp_cert
# Omit DataProtection__CertPassword if the PFX has no password (passout pass: above)Key rotation: when you issue a new certificate, keep the old one in the volume until all existing sessions have expired (ASP.NET Core's default key lifetime is 90 days). The runtime needs the old certificate to decrypt existing keys even after you switch to the new one for writing.
GitHub Actions workflows run on every push and PR:
| Workflow | What it checks |
|---|---|
| CodeQL | Static analysis for C# and TypeScript vulnerabilities |
| Dependency scan | Known CVEs in npm and NuGet packages |
| Docker scan | Container image vulnerability scanning |
| Build validation | Frontend build + backend compile |
| Format check | Prettier (frontend) + dotnet format (backend) |
| Tests | Vitest unit tests (frontend) + xUnit integration tests (backend) |
| Smoke Tests | Playwright E2E browser automation |
We maintain high code quality with automated test coverage enforcement (80% on Codecov backend flag / 70% frontend patch).
- Backend: xUnit + FluentAssertions + WebApplicationFactory integration tests
- Architecture: NetArchTest.Rules — enforces Clean Architecture layer boundaries in CI
- Frontend: Vitest + Angular Testing Library
- E2E/Smoke: Playwright
- Coverage: Coverlet (Cobertura) + ReportGenerator
# From repo root — runs all backend tests (integration + unit + architecture)
dotnet test smooth-operator.slndotnet test smooth-operator.sln \
--settings coverlet.runsettings \
-- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura
reportgenerator -reports:"**/coverage.cobertura.xml" -targetdir:coveragereport -reporttypes:Htmlcd frontend
npm test # watch mode
npm test -- --run # single-run (CI)- Impersonation headers — integration tests inject
X-Test-UserId/X-Test-Rolesheaders to simulate any user role without JWT ceremony. - Real EF with SQLite — integration tests use EF Core + in-memory SQLite for per-test isolation; no mocked repositories.
- Architecture tests —
SmoothOperator.ArchitectureTestsasserts layer dependency rules using NetArchTest so violations fail CI immediately. - Output cache invalidation — write endpoints evict related cache tags (
EvictByTagAsync) so subsequent GETs in tests receive fresh data.
Workflow details: .github/workflows/README.md
Performance work spans the frontend bundle, the API, and the container infrastructure.
- esbuild application builder with fully lazy-loaded feature routes and
OnPushchange detection on list-heavy pages. - Precompressed static assets — JS/CSS/HTML are compressed to
.brand.gzat build time and served via nginxbrotli_static/gzip_static(no per-request compression). Thengx_brotlimodule is compiled in a dedicated Docker build stage. - Bundle analysis —
npm run analyzerenders a local source-map treemap; CI uploads bundle stats to CodeCov for per-PR size tracking.
- EF Core
DbContextPool+ Npgsql connection pooling;AsNoTrackingreads andSplitQuery. - Response compression (Brotli + Gzip) and an output cache (
ShortCachepolicy) — in-memory by default, Redis-backed viaCache__UseRedis. - Trigram search —
pg_trgmexpression GIN indexes make case-insensitive audit-log/user substring filters index-backed instead of sequential scans. - MediatR metrics — every command/query records a Prometheus duration histogram (
smooth_operator_mediatr_request_duration_seconds).
- Tuned PostgreSQL (
shared_buffers, planner costs,wal_compression,pg_stat_statements) and Redis (maxmemory+allkeys-lru, persistence off) viadocker-compose.yml. - Docker — multi-stage builds with BuildKit cache mounts, Alpine runtime images, and per-service resource limits.
- nginx — HTTP/2, an upstream keepalive pool for the API proxy, and tuned worker settings.
- k6 load test —
k6 run load-tests/smoke.jsexercises the auth + connections hot paths (see load-tests/README.md). - BenchmarkDotNet — micro-benchmarks under
backend/benchmarks/SmoothOperator.Benchmarks(run withdotnet run -c Release).
Full user guides, admin reference, integration guides, and API documentation live at smooth-operator-docs.pages.dev (Cloudflare Pages, auto-deployed from master). You can also build and serve the docs locally:
# Start only the docs container
docker compose up docs
# → http://localhost:3000| Section | Contents |
|---|---|
| Getting Started | Installation, first setup, prerequisites |
| User Guide | My Access, active sessions, profile |
| Admin Guide | Users, groups, vaults, connections, credentials, SSO, SMTP |
| API Reference | Interactive Swagger UI + integration examples |