Skip to content

Conversation

@mrmm
Copy link
Contributor

@mrmm mrmm commented Jan 22, 2026

feat: SFTP access control, enforcement mode, file transfer logging, and user role expiry

Closes #1679

Summary

Implements comprehensive SFTP file transfer access control with an instance-wide enforcement mode, audit logging, and user role expiry management. Key changes since the initial PR:

  • Removed legacy SCP protocol — per maintainer feedback, no reason to use SCP in 2026
  • Added SFTP permission enforcement mode (strict/permissive) — in strict mode, shell/exec/port forwarding are blocked for SFTP-restricted targets
  • Added SFTP EXTENDED packet handling — allowlist-based approach for vendor extensions
  • Added advanced restriction UI — allowed paths, blocked extensions, max file size configurable from admin UI
  • Fixed max_file_size enforcement — now checks cumulative bytes during Write operations

Features Implemented

SFTP File Transfer Access Control

Role-Level Defaults

Roles define baseline file transfer permissions that apply to all SSH targets:

  • allow_file_upload / allow_file_download — toggle switches (default: true)
  • allowed_paths — glob patterns restricting accessible directories
  • blocked_extensions — file extensions to block (case-insensitive)
  • max_file_size — maximum upload size in bytes

API: GET/PUT /role/{id}/file-transfer

Target-Role Overrides

Target-role assignments can inherit role defaults OR explicitly override:

  • Inherit (null) — Use role default
  • Allow (true) — Force allow regardless of role default
  • Deny (false) — Force deny regardless of role default

API: GET/PUT /targets/{id}/roles/{role_id}/file-transfer

Hierarchy:

Target-Role Override (explicit) → Role Default → System Default (allow)

SFTP Permission Enforcement Mode (NEW)

Instance-wide sftp_permission_mode setting stored in DB parameters table:

Mode SFTP Restrictions Shell/Exec/Forwarding Use Case
Strict (default) Enforced Blocked when restricted SFTP-only targets, maximum security
Permissive Enforced Allowed (with warning) Audit-only, soft restrictions

In strict mode, when SFTP restrictions are active:

  • Shell (session) channel rejected with: "Shell access is disabled for this target (SFTP-only mode)"
  • Exec channel rejected
  • direct-tcpip and direct-streamlocal channels blocked (prevents SSH tunneling bypass)
  • tcpip-forward and streamlocal-forward requests blocked

SFTP EXTENDED packet handling:

  • Safe extensions always allowed: statvfs@openssh.com, fstatvfs@openssh.com, fsync@openssh.com, limits@openssh.com
  • Write extensions check upload permission: posix-rename@openssh.com, hardlink@openssh.com, lsetstat@openssh.com
  • Unknown extensions blocked when restrictions active, allowed otherwise

API: GET/PUT /parameters (field: sftp_permission_mode)

SCP Protocol Removal

Removed the entire legacy SCP module per maintainer feedback:

  • Deleted warpgate-protocol-ssh/src/scp/ (parser, types, mod — 332 lines)
  • Removed all SCP references from session.rs (~150 lines)
  • Updated doc comments from "SCP/SFTP" to "SFTP"
  • Legacy scp commands now pass through as regular exec commands (blocked in strict mode, unhandled in permissive mode)
  • SCP connection instructions retained in UI for user convenience

File Transfer Audit Logging

All SFTP operations logged with comprehensive details:

  • Event type: file_transfer
  • Metadata: direction, protocol (SFTP), file path, size, SHA256 hash
  • Context: user, role, target, timestamp, status (success/denied)
  • Denied transfers: logged with reason (permission, path, extension, size)

Database: FileTransferLog entity with migration m00027

User Role Expiry

User role assignments can expire automatically:

  • Optional expires_at timestamp on role assignments
  • Expired roles filtered out during authorization
  • Re-activation by updating/removing expiry
  • Full history tracking in UserRoleHistory table

API:

  • POST /users/{id}/roles/{role_id} — Add with optional expiry
  • GET/PUT/DELETE /users/{id}/roles/{role_id}/expiry — Manage expiry
  • GET /users/{id}/roles/{role_id}/history — View audit trail

Admin UI

Global Settings (Parameters):

  • SFTP permission enforcement mode dropdown (strict/permissive)
  • Contextual help text explaining each mode

Role Management:

  • File transfer defaults: upload/download toggles
  • Advanced restrictions (collapsible): allowed paths, blocked extensions, max file size
  • Warning banner when permissive mode + restrictions active

Target Management:

  • Per-role file transfer overrides with 3-state controls (inherit/allow/deny)
  • Advanced restrictions with inheritance display (badges showing inherited values)
  • Reset-to-inherit buttons for each field

User Management:

  • Role expiry status display with time-remaining countdown
  • Quick expiry presets: 4h, 8h, 12h, 1d, 3d, 7d
  • Custom datetime picker for precise control
  • Inline role history section showing all changes

Terraform Provider Support

The companion terraform-provider-warpgate has been updated with full support:

Role defaults:

resource "warpgate_role" "developers" {
  name = "developers"
  
  file_transfer_defaults {
    allow_upload   = true
    allow_download = false
    allowed_paths  = ["/home/deploy", "/var/www"]
    max_file_size  = 104857600  # 100MB
  }
}

Target overrides:

resource "warpgate_target_role" "production_readonly" {
  target_id = warpgate_target.prod_server.id
  role_id   = warpgate_role.developers.id
  
  file_transfer {
    allow_upload   = "false"     # Override: deny uploads
    allow_download = "inherit"   # Inherit from role
  }
}

User role expiry:

resource "warpgate_user_role" "contractor" {
  user_id = warpgate_user.alice.id
  role_id = warpgate_role.temporary.id
  
  expiry {
    expires_at = "2026-12-31T23:59:59Z"
  }
}

Note: The sftp_permission_mode global setting is managed via the Admin UI or direct API call (PUT /parameters), not via Terraform. This is intentional — it's an instance-wide operational setting that affects all targets uniformly.

Technical Details

Database Migrations

  • m00026_scp_sftp_access_control — Initial permission columns
  • m00027_file_transfer_logging — Logging table and indexes
  • m00028_user_role_expiry_history — Expiry and history tracking
  • m00029_role_file_transfer_defaults — Role-level defaults
  • m00030_target_role_nullable_overrides — Inheritance model
  • m00031_user_role_expiry_history — History table refinements
  • m00032_sftp_permission_mode — Enforcement mode column in parameters table

Protocol Implementation

  • SFTP: Intercepts SSH_FXP_OPEN, SSH_FXP_WRITE, SSH_FXP_READ, SSH_FXP_EXTENDED packets
  • SFTP EXTENDED: Allowlist-based handling (safe/write/unknown categories)
  • SCP: Removed — legacy commands pass through as regular exec (blocked in strict mode)
  • Shell/Exec blocking: is_shell_blocked() checks enforcement mode + active restrictions
  • Port forwarding blocking: Same logic applied to all forwarding channel types
  • Max file size: Cumulative byte tracking on Write operations with mid-transfer denial

Testing

  • 28 E2E tests covering all permission scenarios, strict/permissive modes, advanced restrictions
  • 17 unit tests (11 core + 6 SSH) for permission resolution and SFTP codec
  • 220/220 full E2E suite passes (zero regressions)
  • Frontend builds and lints successfully (0 errors)
  • Clippy clean

Performance

  • Minimal overhead: Permission checks cached per session
  • Efficient logging: Async writes with batch inserts
  • Database indexes: Optimized for audit trail queries

Breaking Changes

None — All changes are backward compatible:

  • Default behavior: File transfers allowed (matches current behavior)
  • Default enforcement mode: strict (secure by default, but only activates when restrictions exist)
  • Existing targets: No permissions configured = allow all, shell works normally
  • Existing roles: Defaults to allow upload + download
  • Database: Migrations handle schema updates automatically

Security Considerations

  • Permissions checked at SFTP packet level (cannot be bypassed via protocol)
  • Shell/exec/forwarding blocked in strict mode (prevents SFTP restriction bypass)
  • SFTP EXTENDED packets handled with deny-by-default allowlist
  • Port forwarding blocked to prevent SSH tunneling bypass
  • SHA256 hashes logged for file integrity verification
  • Expired roles filtered during authorization (defense in depth)
  • History table immutable (audit trail integrity)

Documentation

  • OpenAPI schema updated with all new endpoints
  • API client regenerated for frontend
  • Inline help text in admin UI
  • Terraform provider examples
  • Contextual warnings for permissive mode

Testing Instructions

  1. SFTP Permissions (Strict Mode):

    # Set enforcement mode to strict (default)
    curl -X PUT http://localhost:8888/@warpgate/admin/api/parameters \
      -d '{"sftp_permission_mode": "strict"}'
    
    # Create role with upload blocked
    curl -X PUT http://localhost:8888/@warpgate/admin/api/role/dev-role/file-transfer \
      -d '{"allow_file_upload": false, "allow_file_download": true}'
    
    # SFTP upload should be denied
    sftp user@warpgate:prod
    sftp> put file.txt  # → "Permission denied"
    
    # Shell should also be blocked (strict mode)
    ssh user@warpgate:prod  # → "Shell access is disabled for this target (SFTP-only mode)"
    
    # SFTP download should work
    sftp> get file.txt  # → Success
  2. Permissive Mode:

    # Switch to permissive
    curl -X PUT http://localhost:8888/@warpgate/admin/api/parameters \
      -d '{"sftp_permission_mode": "permissive"}'
    
    # SFTP upload still blocked
    sftp> put file.txt  # → "Permission denied"
    
    # But shell now works (with UI warning)
    ssh user@warpgate:prod  # → Shell opens
  3. User Role Expiry:

    # Add role with 1-hour expiry
    curl -X POST http://localhost:8888/@warpgate/admin/api/users/alice/roles/contractor \
      -d '{"expires_at": "2026-01-22T12:00:00Z"}'

Checklist

  • Code follows project style guidelines
  • Self-review completed
  • Comments added for complex logic
  • Documentation updated
  • Tests added/updated (28 E2E + 17 unit)
  • All tests passing (220/220 E2E)
  • No new warnings (clippy clean)
  • Breaking changes documented (N/A)
  • Terraform provider updated
  • OpenAPI schema updated
  • Database migrations tested

Related

Screenshots

Role UI - Upload/Download Toggles

Show Screenshot Role defaults configuration with toggle switches

Target UI - Role Attribution for Upload/Download Override

Show Screenshot Target overrides with inherit/allow/deny dropdowns

User UI - Role Assignment History

Show Screenshot User role assignment history timeline

User UI - Role Expiry Setting

Show Screenshot Expiry setting popup with presets

User UI - Role Expiry Countdown

Show Screenshot Role expiry countdown display

User UI - Make Role Permanent

Show Screenshot Set role to permanent

User UI - Role History

Show Screenshot Role change history log

@mrmm
Copy link
Contributor Author

mrmm commented Jan 22, 2026

Hello @Eugeny, I am not sure why the test aer failing in the Github Action as I have ran them locally and they passed without an issue. Looks like the issue is 2 different test timeout in CI each time due to Docker resource exhaustion after ~160 tests (maybe).
I am open to any proposal!

@Eugeny
Copy link
Member

Eugeny commented Jan 24, 2026

Thank you for the PR! After a cursory glance, it doesn't seem to actually prevent the user from writing/reading data from the FS as there's still nothing stopping them from doing so from within their shell session?

@mrmm
Copy link
Contributor Author

mrmm commented Jan 24, 2026

@Eugeny Thanks fro the feedback, this PR design was mainly around the SCP/SFTP subsystem of SSH, It is possible to intercept the SSH session commands as it is the same-ish mechanism used for recording the SSH session.
I am planning to look into having a feature that allow the administrator to control/prevent some commands to be ran on the Target server.

This PR also does not cover these use-cases too:

  • Allow only the user to upload and/or download file from the server ONLY without SSH session
  • Inline file scanning to prevent any malicious file to be uploaded (only file extensions prevention, which was quick-win as a all the metadata were accessible)

If you think it is mandatory feature to be able to have this in this PR, I will work on it but I will need sometime before being able to deliver this 🙏

Thanks again fro the review

@Eugeny
Copy link
Member

Eugeny commented Jan 24, 2026

I see - unfortunately there's no security gained by this PR as-is unless shell and exec sessions are also forbidden, because there's still (a very easy) way to read/write arbitrary files. So while it might tick the boxes for a specific security audit, it doesn't actually prevent unauthorized access.

It's fundamentally impossible to selectively restrict command execution in shell sessions too, since the SSH server only sees user keystrokes and display output and has no concept of "commands" per se (except exec requests, but again the user can bypass that by simply running commands in the shell).

It's not possible to "recognize" and parse shell input either because that's trivially bypassable by encrypting the commands/inputs, saving it remotely and decrypting it there, so it's never visible on the SSH protocol.

As it is, I'm currently tending towards not accepting this as a feature, however I'm still interested in some parts of this PR, if you could separate them out:

  • Time limited role assignments
  • SFTP audit logging - please see if it's possible to reuse the russh-sftp crate instead of manually parsing the messages

@mrmm
Copy link
Contributor Author

mrmm commented Jan 24, 2026

@Eugeny
I totally agree with you that this technically does not block file transfer as I can open SSH session and copy/paste the content of a file.

I will separate the TTL role assignment feature, and look into the usage of rssh-sftp for the SFTP audit logging, for audit log purposes only !

While I am at it I added some screenshot to this PR, even if it will not move forward I really appreciate the review 🙏

@mrmm
Copy link
Contributor Author

mrmm commented Jan 24, 2026

@Eugeny
Quick update on the russh-sftp usage (and thanks for pointing that out), it would replace:

  • the parser parser.rs
  • response.rs
  • protocol types in types.rs

Follow-up question, would it be acceptable to update this feature to:

  • State that this only blocks, SCP, SFTP and RSYNC protocols from uploading but user has SSH this access can bypassed this ?
    Which gets me to the next point :
  • Is adding the possibility to block interactive SSH sessions which can make the target usable only for File transfer
  • This PR supports also the legacy SCP (that does scp -t and scp -f exec) -> if we move forward with the proposal of blocking interactive sessions

@Eugeny
Copy link
Member

Eugeny commented Jan 26, 2026

Could you please reformulate the second half of your reply? I couldn't parse it 😓

@mrmm
Copy link
Contributor Author

mrmm commented Jan 27, 2026

Sorry @Eugeny for the not clear comment, It was done little bit too lat in the night 😓

To address this, I suggest adding a Restricted Shell mode to make targets truly "File Transfer Only." Regarding this proposal:

  • Interactive Sessions: Should we allow blocking interactive SSH sessions to enforce this file-transfer-only access?
  • UI Clarity: For cases where the role blocks file transfer protocols but not the interactive session, I can add a warning in the UI to let admins know the restriction can be bypassed via the shell.
  • Legacy SCP: I have already implemented support for legacy SCP (scp -t and -f). Should we keep this to ensure full backward compatibility, or would you prefer I drop it to keep this PR more focused and simplified?

@mrmm
Copy link
Contributor Author

mrmm commented Jan 29, 2026

@Eugeny Hello, did you have the time tou check my last comment please ?

@Eugeny
Copy link
Member

Eugeny commented Feb 1, 2026

Thanks for getting back to me and sorry for the delay!

Interactive Sessions: Should we allow blocking interactive SSH sessions to enforce this file-transfer-only access?

Yes - blocking session and exec channels should be a requirement for using SFTP permissions.

UI Clarity: For cases where the role blocks file transfer protocols but not the interactive session, I can add a warning in the UI to let admins know the restriction can be bypassed via the shell.

See above - enabling SFTP permissions should not be possible when interactive sessions are allowed.

Legacy SCP: I have already implemented support for legacy SCP (scp -t and -f). Should we keep this to ensure full backward compatibility, or would you prefer I drop it to keep this PR more focused and simplified?

We can leave these out - no reason to use legacy SCP in 2026.

@mrmm
Copy link
Contributor Author

mrmm commented Feb 7, 2026

@Eugeny Thanks a lot for the clarification I will add those requirements then !
I will update the PR to match the specs, byt the end of the next week 🙏

@mrmm mrmm force-pushed the mrmm/feat-scp-sftp-access-control branch from 3209fca to 0dec921 Compare February 7, 2026 19:36
@mrmm mrmm changed the title feat: SCP/SFTP access control, file transfer logging, and user role expiry feat: SFTP access control, enforcement mode, file transfer logging, and user role expiry Feb 9, 2026
mrmm added 15 commits February 10, 2026 09:11
Implement granular file transfer permissions allowing administrators to
control upload/download access on a per-role-per-target basis.

- Add database migrations for permission columns on target_roles table
- Add SFTP protocol parser with fine-grained operation blocking
- Add SCP command parser for upload/download detection
- Integrate permission checks into SSH session handling
- Add Admin API endpoints for managing file transfer permissions
- Add Admin UI toggles in Target configuration page
- Add E2E tests for permission enforcement
- Add SFTP packet parsing for Open, Read, Write, Close operations
- Add SCP command parsing for upload/download detection
- Implement permission enforcement at packet level in SSH session
- Track file transfers with size and SHA256 hash calculation
- Log file_transfer events with status, direction, protocol, and metadata
- Add pending_reads tracking for accurate SFTP download byte counting
- Update LogViewer UI to display file transfer events with direction arrows
- Add ssh-server container with test user for SCP/SFTP testing
- Update docker-compose with warpgate and ssh-server services
- Ignore docker/data/ directory for persistent test data
- 10 permission tests covering SFTP/SCP upload/download allow/deny scenarios
- 2 logging tests verifying file_transfer events in JSON logs
- Tests use wait_port for reliable warpgate restart handling
- Changed SFTP subsystem to always allow connection for SSH targets
- Permission enforcement now happens at operation level (Open, Read, Write)
- Added detailed error messages including action and target name
- Example: 'Permission denied: file upload is not allowed on target prod-server'

This improves UX when users have restricted file transfer permissions,
replacing cryptic 'subsystem request failed' errors with actionable messages.
Implements time-limited user role assignments with automatic tracking of all role changes in a history table. Roles can have an optional expiry timestamp and all modifications (granted, revoked, expiry changes) are recorded with actor information and timestamps.

Features:
- User role assignments can expire at a specified timestamp
- Expired roles are filtered out during authorization
- Full history tracking with UserRoleHistory entity
- API endpoints for managing and viewing role expiry
- Re-enable expired roles by updating/removing expiry
Adds default file upload/download permissions at the role level that can be inherited or overridden by target-role assignments. Roles now define baseline file transfer policies that apply to all SSH targets unless explicitly overridden.

Features:
- Role-level allowFileUpload and allowFileDownload flags
- API endpoints to get and update role file transfer defaults
- Defaults are inherited by target-role assignments when not overridden
Allows targets to override role file transfer defaults with three-way options (inherit/allow/deny) for upload and download permissions separately. This enables fine-grained control where specific targets can restrict or permit file transfers regardless of role defaults.

Features:
- Nullable allow_file_upload and allow_file_download columns
- null = inherit from role, true = force allow, false = force deny
- API endpoints to get and update target-role file transfer permissions
- Updated authorization logic to respect override hierarchy
- E2E tests for file transfer permission scenarios
Implements comprehensive UI for managing user role assignments with time-based expiry and viewing historical changes.

Features:
- Quick expiry presets (4h, 8h, 12h, 1d, 3d, 7d) for common durations
- Custom datetime picker for precise expiry control
- Real-time expiry status display (e.g., "Expires in 3h 59min")
- Expired roles treated as unassigned and can be re-enabled
- Re-enabling expired role removes expiry (makes permanent)
- Inline role history section with load more pagination
- History shows all role changes with actor and timestamp information
- Modal with improved layout and horizontal button arrangement
Adds UI for managing role-level file transfer defaults and target-level permission overrides with clean, compact controls.

Features:
- Role page: Simple checkboxes for default upload/download permissions
- Target page: Collapsible file transfer section per role
- Three-way select dropdowns (Inherit/Allow/Deny) for upload and download
- Collapsed state shows current permission summary
- Compact horizontal layout with input groups
- Shows inherited role defaults in select options for clarity
Add 100ms delay after SSH server startup to allow full initialization
in resource-constrained CI environments. This may help prevent timeout
issues when many Docker containers are running concurrently.

Tests continue to pass locally (12/12 in 14s).
Ignore development environment files:
- AGENTS.md (OpenCode agent instructions)
- mise.toml (personal mise configuration)
- .github/workflows/opencode.yml (local workflow)
- *.log files (session logs)
- thoughts/ directory (documentation drafts)
- docker/*.sample.yaml (personal config samples)
Migrate from custom SFTP parser (~590 lines) to russh-sftp 2.1 codec wrapper
(~150 lines), achieving 49% code reduction in the SFTP module.

Changes:
- Add russh-sftp 2.1 dependency
- Create new codec.rs with packet_to_operation(), packet_to_response(),
  and build_denial_response() wrappers
- Remove parser.rs (465 lines) and response.rs (126 lines)
- Update session.rs to use codec functions instead of custom parser
- Simplify types.rs by removing redundant type definitions

The russh-sftp library provides proper SFTP protocol parsing while
Warpgate's codec.rs handles access control and metadata extraction.

Refs: thoughts/plans/russh-sftp_migration.md
mrmm added 14 commits February 10, 2026 09:13
Remove the entire SCP module (parser, types, mod) and all SCP references
from session.rs. SCP is obsolete — legacy scp commands now pass through
as regular exec commands with no special handling.

Update doc comments from 'SCP/SFTP' to 'SFTP' in entity definitions,
API docs, and config providers.
… blocking

Add instance-wide sftp_permission_mode setting (strict/permissive) stored
in DB parameters table. In strict mode, shell, exec, and port forwarding
channels are blocked for targets with SFTP restrictions, making them
truly SFTP-only. In permissive mode, SFTP rules are enforced but
shell access remains available.

- Add DB migration for sftp_permission_mode column
- Add shell_blocked field to FileTransferPermission with 6 unit tests
- Block shell, exec, direct-tcpip, direct-streamlocal, tcpip-forward,
  and streamlocal-forward channels when restrictions are active
- Handle SFTP EXTENDED packets with safe/write/unknown allowlist
- Add max_file_size enforcement on Write operations
- Add advanced restriction UI (allowed_paths, blocked_extensions,
  max_file_size) to Role.svelte and Target.svelte
- Add enforcement mode dropdown to Parameters.svelte
- Add permissive mode warning banners to Role/Target UI
- CredentialEditor: fix curly braces, use SvelteSet instead of Set
- RoleHistoryModal: handle PaginatedResponse type, fix Date formatting
- CreateApiTokenModal: remove semicolons from toLocalISO function
- User.svelte: fix lint warnings
- Rust API files: minor fixes for consistency
Add 28 E2E tests covering:
- File transfer permissions (default, download, upload, API, multi-role)
- Transfer logging and denied transfer logging
- Strict mode (shell blocked, shell allowed, SFTP works, port forwarding)
- Permissive mode (shell allowed, SFTP enforced)
- Advanced restrictions (allowed_paths, blocked_extensions, case sensitivity)
- Role-level defaults and target-role overrides
- Exec and remote forwarding blocking in strict mode

Fix test_ssh_client_auth_config.py: add missing AddUserRoleRequest
argument required by updated SDK from user role expiry feature.
Update bytes 1.11.0->1.11.1, time 0.3.44->0.3.47, thiserror 2.0.17->2.0.18
to resolve cargo-deny vulnerability warnings in CI.
…ssignment

- Show expired roles with strikethrough, reduced opacity, and red Expired badge
- Add Re-enable button for expired roles that opens expiry modal
- Inline expiry modal on new role assignment (permanent default, presets)
- Add labeled Expiry button (clock icon + text) replacing icon-only
- Past-date validation on datetime-local input with min constraint
- Live countdown timer (60s interval) showing time remaining
- Confirmation dialog before revoking a role
- Auto-load role history on first expand
- Add 30-day preset to expiry options
- 14 role expiry tests: grant with TTL, expired denied, re-enable,
  revoke, reactivate, history audit trail, API state listing
- 18 SFTP operation tests: remove, rename, mkdir, rmdir, setstat,
  symlink, max_file_size enforcement, extended packets (statvfs),
  metadata ops, and strict mode streamlocal blocking
@mrmm mrmm force-pushed the mrmm/feat-scp-sftp-access-control branch from 8330df8 to 040f7d2 Compare February 10, 2026 09:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: SCP/SFTP Access Control and File Transfer Logging and Expiry

2 participants