Repository: emberstack/docker-sftp
Branch: main
Commit: b9adb0684803
Files: 69
Total size: 100.3 KB
Directory structure:
gitextract_0tjh4w4i/
├── .dockerignore
├── .github/
│ ├── dependabot.yml
│ ├── stale.yml
│ └── workflows/
│ └── pipeline.yaml
├── .gitignore
├── LICENSE
├── README.md
├── samples/
│ ├── .ssh/
│ │ ├── id_demo2_ed25519
│ │ ├── id_demo2_ed25519.pub
│ │ ├── id_demo2_rsa
│ │ ├── id_demo2_rsa.pub
│ │ ├── id_demo_rsa
│ │ └── id_demo_rsa.pub
│ ├── hooks/
│ │ ├── onsessionchange
│ │ └── onstartup
│ ├── sample.dev.sftp.json
│ └── sample.sftp.json
└── src/
├── .dockerignore
├── ES.SFTP/
│ ├── Api/
│ │ └── PamEventsController.cs
│ ├── Configuration/
│ │ ├── ConfigurationService.cs
│ │ └── Elements/
│ │ ├── ChrootDefinition.cs
│ │ ├── GlobalConfiguration.cs
│ │ ├── GroupDefinition.cs
│ │ ├── HooksDefinition.cs
│ │ ├── HostKeysDefinition.cs
│ │ ├── LoggingDefinition.cs
│ │ ├── SftpConfiguration.cs
│ │ └── UserDefinition.cs
│ ├── Dockerfile
│ ├── ES.SFTP.csproj
│ ├── Extensions/
│ │ └── DirectoryInfoExtensions.cs
│ ├── Interop/
│ │ ├── ProcessRunOutput.cs
│ │ └── ProcessUtil.cs
│ ├── Messages/
│ │ ├── Configuration/
│ │ │ └── SftpConfigurationRequest.cs
│ │ ├── Events/
│ │ │ ├── ConfigurationChanged.cs
│ │ │ ├── ServerStartupEvent.cs
│ │ │ └── UserSessionStartedEvent.cs
│ │ └── Pam/
│ │ └── PamEventRequest.cs
│ ├── Program.cs
│ ├── Properties/
│ │ └── launchSettings.json
│ ├── SSH/
│ │ ├── Configuration/
│ │ │ ├── MatchBlock.cs
│ │ │ └── SSHConfiguration.cs
│ │ ├── HookRunner.cs
│ │ ├── SSHService.cs
│ │ └── SessionHandler.cs
│ ├── Security/
│ │ ├── AuthenticationService.cs
│ │ ├── GroupUtil.cs
│ │ ├── UserManagementService.cs
│ │ └── UserUtil.cs
│ ├── app.logging.Development.json
│ ├── app.logging.json
│ ├── appsettings.Development.json
│ ├── appsettings.json
│ └── config/
│ ├── sftp.json
│ └── sssd.conf
├── ES.SFTP.sln
├── ES.SFTP.sln.DotSettings
├── docker-compose/
│ ├── docker-compose.override.dev.yaml
│ └── docker-compose.yaml
└── helm/
└── sftp/
├── .helmignore
├── Chart.yaml
├── LICENSE
├── templates/
│ ├── NOTES.txt
│ ├── _helpers.tpl
│ ├── config-secret.yaml
│ ├── deployment.yaml
│ ├── service.yaml
│ └── serviceaccount.yaml
└── values.yaml
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: nuget
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
================================================
FILE: .github/stale.yml
================================================
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 7
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 7
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
onlyLabels: []
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- pinned
- security
- "[Status] Maybe Later"
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: false
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: false
# Set to true to ignore issues with an assignee (defaults to false)
exemptAssignees: false
# Label to use when marking as stale
staleLabel: stale
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
Automatically marked as stale due to no recent activity.
It will be closed if no further activity occurs. Thank you for your contributions.
# Comment to post when removing the stale label.
unmarkComment: >
Removed stale label.
# Comment to post when closing a stale Issue or Pull Request.
closeComment: >
Automatically closed stale item.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30
# Limit to only `issues` or `pulls`
# only: issues
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
# pulls:
# daysUntilStale: 30
# markComment: >
# This pull request has been automatically marked as stale because it has not had
# recent activity. It will be closed if no further activity occurs. Thank you
# for your contributions.
# issues:
# exemptLabels:
# - confirmed
================================================
FILE: .github/workflows/pipeline.yaml
================================================
name: Pipeline
on:
push:
paths:
- "src/**"
- ".github/workflows/**"
pull_request:
paths:
- "src/**"
- ".github/workflows/**"
env:
version: 5.1.${{github.run_number}}
imageRepository: "emberstack/sftp"
DOCKER_CLI_EXPERIMENTAL: "enabled"
jobs:
ci:
name: CI
runs-on: ubuntu-latest
steps:
- name: tools - helm - install
uses: azure/setup-helm@v1
- name: checkout
uses: actions/checkout@v2
- name: artifacts - prepare directories
run: |
mkdir -p .artifacts/helm
- name: helm - import README
run: cp README.md src/helm/sftp/README.md
- name: helm - package chart
run: helm package --destination .artifacts/helm --version ${{env.version}} --app-version ${{env.version}} src/helm/sftp
- name: "artifacts - upload - helm chart"
uses: actions/upload-artifact@v2
with:
name: helm
path: .artifacts/helm
- name: tools - docker - login
if: github.event_name == 'push'
uses: docker/login-action@v1
with:
username: ${{ secrets.ES_DOCKERHUB_USERNAME }}
password: ${{ secrets.ES_DOCKERHUB_PAT }}
- name: "docker - buildx prepare"
run: |
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
docker buildx create --name builder --driver docker-container --use
docker buildx inspect --bootstrap
- name: "docker - build PR"
if: github.event_name == 'pull_request'
run: |
docker buildx build --platform linux/amd64 -t ${{env.imageRepository}}:build-${{env.version}}-amd64 -f src/ES.SFTP/Dockerfile src/
docker buildx build --platform linux/arm -t ${{env.imageRepository}}:build-${{env.version}}-arm32v7 -f src/ES.SFTP/Dockerfile src/
docker buildx build --platform linux/arm64 -t ${{env.imageRepository}}:build-${{env.version}}-arm64v8 -f src/ES.SFTP/Dockerfile src/
- name: "docker - build and publish - amd64"
if: github.event_name == 'push'
run: |
docker buildx build --push --platform linux/amd64 --provenance=false -t ${{env.imageRepository}}:build-${{env.version}}-amd64 -f src/ES.SFTP/Dockerfile src/
- name: "docker - build and publish - arm32v7"
if: github.event_name == 'push'
run: |
docker buildx build --push --platform linux/arm --provenance=false -t ${{env.imageRepository}}:build-${{env.version}}-arm32v7 -f src/ES.SFTP/Dockerfile src/
- name: "docker - build and publish - arm64v8"
if: github.event_name == 'push'
run: |
docker buildx build --push --platform linux/arm64 --provenance=false -t ${{env.imageRepository}}:build-${{env.version}}-arm64v8 -f src/ES.SFTP/Dockerfile src/
- name: "docker - create manifest and publish"
if: github.event_name == 'push'
run: |
docker pull --platform linux/amd64 ${{env.imageRepository}}:build-${{env.version}}-amd64
docker pull --platform linux/arm/v7 ${{env.imageRepository}}:build-${{env.version}}-arm32v7
docker pull --platform linux/arm64 ${{env.imageRepository}}:build-${{env.version}}-arm64v8
docker manifest create ${{env.imageRepository}}:build-${{env.version}} ${{env.imageRepository}}:build-${{env.version}}-amd64 ${{env.imageRepository}}:build-${{env.version}}-arm32v7 ${{env.imageRepository}}:build-${{env.version}}-arm64v8
docker manifest inspect ${{env.imageRepository}}:build-${{env.version}}
docker manifest push ${{env.imageRepository}}:build-${{env.version}}
cd:
name: CD
needs: ci
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: tools - helm - install
uses: azure/setup-helm@v1
- name: tools - docker - login
uses: docker/login-action@v1
with:
username: ${{ secrets.ES_DOCKERHUB_USERNAME }}
password: ${{ secrets.ES_DOCKERHUB_PAT }}
- name: artifacts - download - helm chart
uses: actions/download-artifact@v2
with:
name: helm
path: .artifacts/helm
- name: "docker - create manifest and publish"
run: |
docker pull ${{env.imageRepository}}:build-${{env.version}}-amd64
docker pull ${{env.imageRepository}}:build-${{env.version}}-arm32v7
docker pull ${{env.imageRepository}}:build-${{env.version}}-arm64v8
docker manifest create ${{env.imageRepository}}:${{env.version}} ${{env.imageRepository}}:build-${{env.version}}-amd64 ${{env.imageRepository}}:build-${{env.version}}-arm32v7 ${{env.imageRepository}}:build-${{env.version}}-arm64v8
docker manifest create ${{env.imageRepository}}:latest ${{env.imageRepository}}:build-${{env.version}}-amd64 ${{env.imageRepository}}:build-${{env.version}}-arm32v7 ${{env.imageRepository}}:build-${{env.version}}-arm64v8
docker manifest push ${{env.imageRepository}}:${{env.version}}
docker manifest push ${{env.imageRepository}}:latest
docker manifest push ${{env.imageRepository}}:${{env.version}}
docker manifest push ${{env.imageRepository}}:latest
docker tag ${{env.imageRepository}}:build-${{env.version}}-amd64 ${{env.imageRepository}}:${{env.version}}-amd64
docker tag ${{env.imageRepository}}:build-${{env.version}}-arm32v7 ${{env.imageRepository}}:${{env.version}}-arm32v7
docker tag ${{env.imageRepository}}:build-${{env.version}}-arm64v8 ${{env.imageRepository}}:${{env.version}}-arm64v8
docker push ${{env.imageRepository}}:${{env.version}}-amd64
docker push ${{env.imageRepository}}:${{env.version}}-arm32v7
docker push ${{env.imageRepository}}:${{env.version}}-arm64v8
- name: github - checkout - helm-charts
uses: actions/checkout@v2
with:
repository: emberstack/helm-charts
token: ${{ secrets.ES_GITHUB_PAT }}
path: helm-charts
ref: main
- name: github - publish - chart
run: |
mkdir -p helm-charts/repository/sftp
cp .artifacts/helm/sftp-${{env.version}}.tgz helm-charts/repository/sftp
cd helm-charts
git config user.name "Romeo Dumitrescu"
git config user.email "5931333+winromulus@users.noreply.github.com"
git add .
git status
git commit -m "Added sftp-${{env.version}}.tgz"
git push
- name: github - create release
uses: softprops/action-gh-release@v1
with:
tag_name: v${{env.version}}
body: The release process is automated.
token: ${{ secrets.ES_GITHUB_PAT }}
================================================
FILE: .gitignore
================================================
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# **/Properties/launchSettings.json
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 emberstack
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# SFTP ([SSH File Transfer Protocol](https://en.wikipedia.org/wiki/SSH_File_Transfer_Protocol)) server using [OpenSSH](https://en.wikipedia.org/wiki/OpenSSH)
This project provides a Docker image for hosting a SFTP server. Included are `Docker` (`docker-cli` and `docker-compose`) and `Kubernetes` (`kubectl` and `helm`) deployment scripts
[](https://github.com/emberstack/docker-sftp/actions/workflows/pipeline.yaml)
[](https://github.com/emberstack/docker-sftp/releases/latest)
[](https://hub.docker.com/r/emberstack/sftp)
[](https://hub.docker.com/r/emberstack/sftp)
[](LICENSE)
> Supports architectures: `amd64`, `arm` and `arm64`
### Support
If you need help or found a bug, please feel free to open an issue on the [emberstack/docker-sftp](https://github.com/emberstack/docker-sftp) GitHub project.
## Usage
The SFTP server can be easily deployed to any platform that can host containers based on Docker.
Below are deployment methods for:
- Docker CLI
- Docker-Compose
- Kubernetes using Helm (recommended for Kubernetes)
Process:
1) Create server configuration
2) Mount volumes as needed
3) Set host file for consistent server fingerprint
### Configuration
The SFTP server uses a `json` based configuration file for default server options and to define users. This file has to be mounted on `/app/config/sftp.json` inside the container.
Environment variable based configuration is not supported (see the `Advanced Configuration` section below for the reasons).
Below is the simplest configuration file for the SFTP server:
```json
{
"Global": {
"Chroot": {
"Directory": "%h",
"StartPath": "sftp"
},
"Directories": ["sftp"]
},
"Users": [
{
"Username": "demo",
"Password": "demo"
}
]
}
```
This configuration creates a user `demo` with the password `demo`.
A directory "sftp" is created for each user in the own home and is accessible for read/write.
The user is `chrooted` to the `/home/demo` directory. Upon connect, the start directory is `sftp`.
You can add additional users, default directories or customize start directories per user. You can also define the `UID` and `GID` for each user. See the `Advanced Configuration` section below for all configuration options.
### Deployment using Docker CLI
> Simple Docker CLI run
```shellsession
$ docker run -p 22:22 -d emberstack/sftp --name sftp
```
This will start a SFTP in the container `sftp` with the default configuration. You can connect to it and login with the `user: demo` and `password: demo`.
> Provide your configuration
```shellsession
$ docker run -p 22:22 -d emberstack/sftp --name sftp -v /host/sftp.json:/app/config/sftp.json:ro
```
This will override the default (`/app/config/sftp.json`) configuration with the one from the host `/host/sftp.json`.
> Mount a directory from the host for the user 'demo'
```shellsession
$ docker run -p 22:22 -d emberstack/sftp --name sftp -v /host/sftp.json:/app/config/sftp.json:ro -v /host/demo:/home/demo/sftp
```
This will mount the `demo` directory from the host on the `sftp` directory for the "demo" user.
### Deployment using Docker Compose
> Simple docker-compose configuration
Create a docker-compose configuration file:
```yaml
version: '3'
services:
sftp:
image: "emberstack/sftp"
ports:
- "22:22"
volumes:
- ../config-samples/sample.sftp.json:/app/config/sftp.json:ro
```
And run it using docker-compose
```shellsession
$ docker-compose -p sftp -f docker-compose.yaml up -d
```
The above configuration is available in the `deploy\docker-compose` folder in this repository. You can use it to start customizing the deployment for your environment.
### Deployment to Kubernetes using Helm
Use Helm to install the latest released chart:
```shellsession
$ helm repo add emberstack https://emberstack.github.io/helm-charts
$ helm repo update
$ helm upgrade --install sftp emberstack/sftp
```
You can customize the values of the helm deployment by using the following Values:
| Parameter | Description | Default |
| ------------------------------------ | -------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `nameOverride` | Overrides release name | `""` |
| `fullnameOverride` | Overrides release fullname | `""` |
| `image.repository` | Container image repository | `emberstack/sftp` |
| `image.tag` | Container image tag | `latest` |
| `image.pullPolicy` | Container image pull policy | `Always` if `image.tag` is `latest`, else `IfNotPresent`|
| `storage.volumes` | Defines additional volumes for the pod | `{}` |
| `storage.volumeMounts` | Defines additional volumes mounts for the sftp container | `{}` |
| `configuration` | Allows the in-line override of the configuration values | `null` |
| `configuration.Global.Chroot.Directory` | Global chroot directory for the `sftp` user group. Can be overriden per-user | `"%h"` |
| `configuration.Global.Chroot.StartPath` | Start path for the `sftp` user group. Can be overriden per-user | `"sftp"` |
| `configuration.Global.Directories` | Directories that get created for all `sftp` users. Can be appended per user | `["sftp"]` |
| `configuration.Global.HostKeys.Ed25519` | Set the server's ED25519 private key | `""` |
| `configuration.Global.HostKeys.Rsa` | Set the server's RSA private key | `""` |
| `configuration.Users` | Array of users and their properties | Contains `demo` user by default |
| `configuration.Users[].Username` | Set the user's username | N/A |
| `configuration.Users[].Password` | Set the user's password. If empty or `null`, password authentication is disabled | N/A |
| `configuration.Users[].PasswordIsEncrypted` | `true` or `false`. Indicates if the password value is already encrypted | `false` |
| `configuration.Users[].AllowedHosts` | Set the user's allowed hosts. If empty, any host is allowed | `[]` |
| `configuration.Users[].PublicKeys` | Set the user's public keys | `[]` |
| `configuration.Users[].UID` | Sets the user's UID. | `null` |
| `configuration.Users[].GID` | Sets the user's GID. A group is created for this value and the user is included | `null` |
| `configuration.Users[].Chroot` | If set, will override global `Chroot` settings for this user. | `null` |
| `configuration.Users[].Directories` | Array of additional directories created for this user | `null` |
| `initContainers` | Additional initContainers for the pod | `{}` |
| `resources` | Resource limits | `{}` |
| `nodeSelector` | Node labels for pod assignment | `{}` |
| `tolerations` | Toleration labels for pod assignment | `[]` |
| `affinity` | Node affinity for pod assignment | `{}` |
> Find us on [Helm Hub](https://hub.helm.sh/charts/emberstack)
## Advanced Configuration
TODO: This section is under development due to the number of configuration options being added. Please open an issue on the [emberstack/docker-sftp](https://github.com/emberstack/docker-sftp) project if you need help.
================================================
FILE: samples/.ssh/id_demo2_ed25519
================================================
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACCItsK7CZxhI38h+dvuQOSbUZpIV84n7QAmt7XXONbxLQAAAIgMsBerDLAX
qwAAAAtzc2gtZWQyNTUxOQAAACCItsK7CZxhI38h+dvuQOSbUZpIV84n7QAmt7XXONbxLQ
AAAECGtcsqvGH3fXmxHiuFdK+qYJsJrTpHVP6CCEPnMGByDIi2wrsJnGEjfyH52+5A5JtR
mkhXziftACa3tdc41vEtAAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----
================================================
FILE: samples/.ssh/id_demo2_ed25519.pub
================================================
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIi2wrsJnGEjfyH52+5A5JtRmkhXziftACa3tdc41vEt
================================================
FILE: samples/.ssh/id_demo2_rsa
================================================
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----
================================================
FILE: samples/.ssh/id_demo2_rsa.pub
================================================
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC0k+rw4Z4C7VRsjLNbsywtkrRB3JDE0AOSn9U3AqF6fbId6SK1ft6G+XrJUqJCJSB7Z6kzIfwk+qr6/gQ4QrAJqQ/s5DGPn3B1fPU5T+B/U3HvMzPdKwktA9/fG+XxrT/HMjltxyCXO5W7EYYxjnE5cuF4ULHRuyJ2+wPBJIdxgWZFGnmakKLYJhhZ2taecN+O0vdjrUFsYJ54YR95+6zK3VBfDIDot7xsPhlsIMHrH/+ZeSR/sBFXfVVwA5oVqOQ8xEnw0EM7m1tHWmGs4+HbOxmkEVNm5P/a5V1p0nJSM8uW8MloomCCqwY8uGixOhkTcAe1iFUVKALPx+Xhyk/5xOdwBHgPjSTXV5toHU0b5l6MHTjao/bFI+EBVXmKidacBhVbluVDRsT7WR3TAPp/qPz9I2tEFZJM2tUe+vynueOdkXXKR6uzF/IeJvkAZqaG2tuqh7HpLvfAMY5RlFxMtvcDhzjVPAC0Bc16vcapS12hEc4LqwvIqqUVgotAbrU= winromulus@Maximus
================================================
FILE: samples/.ssh/id_demo_rsa
================================================
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----
================================================
FILE: samples/.ssh/id_demo_rsa.pub
================================================
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC++8/LkNAu1DPEjnBhzjTF3dkFY+jbRDIsQ/2JkGpRdEHmcCMBTMZGL9PCEEGjWo1Lfocfnk5hWrcloTMCh+rD5VCFNyPCEePK6nyEzZHwcQk9t6dWQwyjtLG8uAhVA30sl0Uw48YcNl9aF8FzpPMWnC7omM2VQPqq0Le05Hu50q0rW97z0vnxpQe+gdNhXOTq0FQ+J2wCwGc7Lxn5uXmZEozmZvlyFVEw6eFlyo9BwLluTHqd5wh9z+jx2U8dQfnIofrgd2Dp86tGNnvS59L/T/0llP8mbvTZNfusMJiO4gNNlsYhj4lQxUQaDL7gy9fxl8Pqf0eGnpOXluSMpAET1oFY5kKgHbfl6peepZzPQ77LQZDNDkrTwqc47VDNlxkdBmV9mp1R+C6no8Ws1Rkk+xYoNbXy6wVOEZy6VSydOy1OsUPpc1hMALYtkxNs88RBeVi/2uQZ8ssXwyKhTIs4zB0JXnSJbOrnkE/NiR8m6r7Nj21oRPcg0Jihl6gq7nU= winromulus@Maximus
================================================
FILE: samples/hooks/onsessionchange
================================================
#!/bin/bash
echo "Session event '$1' for '$2'"
================================================
FILE: samples/hooks/onstartup
================================================
#!/bin/bash
echo "SSH service startup hook completed."
================================================
FILE: samples/sample.dev.sftp.json
================================================
{
"Global": {
"Chroot": {
"Directory": "%h",
"StartPath": "sftp"
},
"Directories": ["sftp"],
"HostKeys": {
"Ed25519": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACBvlz4T2Fh9PKKeVhSupzXsBYVt44VJcb1554gRLKS2oAAAAIiJdbTtiXW0\n7QAAAAtzc2gtZWQyNTUxOQAAACBvlz4T2Fh9PKKeVhSupzXsBYVt44VJcb1554gRLKS2oA\nAAAEDI/igTE3dx3UC0As1d4kL0BNDaA3MkO9lDyWXqfErITm+XPhPYWH08op5WFK6nNewF\nhW3jhUlxvXnniBEspLagAAAAAAECAwQF\n-----END OPENSSH PRIVATE KEY-----\n"
}
},
"Users": [
{
"Username": "demo",
"Password": "demo",
"PublicKeys": [
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC++8/LkNAu1DPEjnBhzjTF3dkFY+jbRDIsQ/2JkGpRdEHmcCMBTMZGL9PCEEGjWo1Lfocfnk5hWrcloTMCh+rD5VCFNyPCEePK6nyEzZHwcQk9t6dWQwyjtLG8uAhVA30sl0Uw48YcNl9aF8FzpPMWnC7omM2VQPqq0Le05Hu50q0rW97z0vnxpQe+gdNhXOTq0FQ+J2wCwGc7Lxn5uXmZEozmZvlyFVEw6eFlyo9BwLluTHqd5wh9z+jx2U8dQfnIofrgd2Dp86tGNnvS59L/T/0llP8mbvTZNfusMJiO4gNNlsYhj4lQxUQaDL7gy9fxl8Pqf0eGnpOXluSMpAET1oFY5kKgHbfl6peepZzPQ77LQZDNDkrTwqc47VDNlxkdBmV9mp1R+C6no8Ws1Rkk+xYoNbXy6wVOEZy6VSydOy1OsUPpc1hMALYtkxNs88RBeVi/2uQZ8ssXwyKhTIs4zB0JXnSJbOrnkE/NiR8m6r7Nj21oRPcg0Jihl6gq7nU= winromulus@Maximus"
]
},
{
"Username": "demo2",
"Password": "demo2"
}
]
}
================================================
FILE: samples/sample.sftp.json
================================================
{
"Global": {
"Chroot": {
"Directory": "%h",
"StartPath": "sftp"
},
"Directories": ["sftp"]
},
"Users": [
{
"Username": "demo",
"Password": "demo"
}
]
}
================================================
FILE: src/.dockerignore
================================================
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
================================================
FILE: src/ES.SFTP/Api/PamEventsController.cs
================================================
using ES.SFTP.Messages.Pam;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace ES.SFTP.Api;
[Route("api/events/pam")]
public class PamEventsController : Controller
{
private readonly ILogger<PamEventsController> _logger;
private readonly IMediator _mediator;
public PamEventsController(ILogger<PamEventsController> logger, IMediator mediator)
{
_logger = logger;
_mediator = mediator;
}
[HttpGet]
[Route("generic")]
public async Task<IActionResult> OnGenericPamEvent(string username, string type, string service)
{
_logger.LogDebug("Received event for user '{username}' with type '{type}', {service}",
username, type, service);
var response = await _mediator.Send(new PamEventRequest
{
Username = username,
EventType = type,
Service = service
});
return response ? Ok() : BadRequest();
}
}
================================================
FILE: src/ES.SFTP/Configuration/ConfigurationService.cs
================================================
using ES.SFTP.Configuration.Elements;
using ES.SFTP.Messages.Configuration;
using ES.SFTP.Messages.Events;
using MediatR;
using Microsoft.Extensions.Options;
namespace ES.SFTP.Configuration;
public class ConfigurationService : IHostedService, IRequestHandler<SftpConfigurationRequest, SftpConfiguration>
{
private readonly ILogger _logger;
private readonly IMediator _mediator;
private readonly IOptionsMonitor<SftpConfiguration> _sftpOptionsMonitor;
private SftpConfiguration _config;
private IDisposable _sftpOptionsMonitorChangeHandler;
public ConfigurationService(ILogger<ConfigurationService> logger,
IOptionsMonitor<SftpConfiguration> sftpOptionsMonitor,
IMediator mediator)
{
_logger = logger;
_sftpOptionsMonitor = sftpOptionsMonitor;
_mediator = mediator;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogDebug("Starting");
_sftpOptionsMonitorChangeHandler = _sftpOptionsMonitor.OnChange(OnSftpConfigurationChanged);
await UpdateConfiguration();
_logger.LogInformation("Started");
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogDebug("Stopping");
_sftpOptionsMonitorChangeHandler?.Dispose();
_logger.LogInformation("Stopped");
return Task.CompletedTask;
}
public Task<SftpConfiguration> Handle(SftpConfigurationRequest request, CancellationToken cancellationToken)
{
return Task.FromResult(_config);
}
private void OnSftpConfigurationChanged(SftpConfiguration arg1, string arg2)
{
_logger.LogInformation("SFTP Configuration was changed.");
UpdateConfiguration().Wait();
_mediator.Publish(new ConfigurationChanged()).ConfigureAwait(false);
}
private Task UpdateConfiguration()
{
_logger.LogDebug("Validating and updating configuration");
var config = _sftpOptionsMonitor.CurrentValue ?? new SftpConfiguration();
config.Global ??= new GlobalConfiguration();
config.Global.Directories ??= new List<string>();
config.Global.Logging ??= new LoggingDefinition();
config.Global.Chroot ??= new ChrootDefinition();
config.Global.PKIandPassword ??= new string("");
config.Global.HostKeys ??= new HostKeysDefinition();
config.Global.Hooks ??= new HooksDefinition();
if (string.IsNullOrWhiteSpace(config.Global.Chroot.Directory)) config.Global.Chroot.Directory = "%h";
if (string.IsNullOrWhiteSpace(config.Global.Chroot.StartPath)) config.Global.Chroot.StartPath = null;
config.Users ??= new List<UserDefinition>();
var validUsers = new List<UserDefinition>();
for (var index = 0; index < config.Users.Count; index++)
{
var userDefinition = config.Users[index];
if (string.IsNullOrWhiteSpace(userDefinition.Username))
{
_logger.LogWarning("Users[{index}] has a null or whitespace username. Skipping user.", index);
continue;
}
userDefinition.Chroot ??= new ChrootDefinition();
if (string.IsNullOrWhiteSpace(userDefinition.Chroot.Directory))
userDefinition.Chroot.Directory = config.Global.Chroot.Directory;
if (string.IsNullOrWhiteSpace(userDefinition.Chroot.StartPath))
userDefinition.Chroot.StartPath = config.Global.Chroot.StartPath;
if (userDefinition.Chroot.Directory == config.Global.Chroot.Directory &&
userDefinition.Chroot.StartPath == config.Global.Chroot.StartPath)
userDefinition.Chroot = null;
userDefinition.Directories ??= new List<string>();
validUsers.Add(userDefinition);
}
config.Users = validUsers;
_logger.LogInformation("Configuration contains '{userCount}' user(s)", config.Users.Count);
_config = config;
return Task.CompletedTask;
}
}
================================================
FILE: src/ES.SFTP/Configuration/Elements/ChrootDefinition.cs
================================================
namespace ES.SFTP.Configuration.Elements;
public class ChrootDefinition
{
public string Directory { get; set; } = "%h";
public string StartPath { get; set; }
}
================================================
FILE: src/ES.SFTP/Configuration/Elements/GlobalConfiguration.cs
================================================
namespace ES.SFTP.Configuration.Elements;
public class GlobalConfiguration
{
public ChrootDefinition Chroot { get; set; } = new();
public List<string> Directories { get; set; } = new();
public LoggingDefinition Logging { get; set; } = new();
public HostKeysDefinition HostKeys { get; set; } = new();
public HooksDefinition Hooks { get; set; } = new();
public string PKIandPassword { get; set; }
public string Ciphers { get; set; }
public string HostKeyAlgorithms { get; set; }
public string KexAlgorithms { get; set; }
public string MACs { get; set; }
}
================================================
FILE: src/ES.SFTP/Configuration/Elements/GroupDefinition.cs
================================================
namespace ES.SFTP.Configuration.Elements;
public class GroupDefinition
{
public string Name { get; set; }
public int? GID { get; set; }
public List<string> Users { get; set; } = new();
}
================================================
FILE: src/ES.SFTP/Configuration/Elements/HooksDefinition.cs
================================================
namespace ES.SFTP.Configuration.Elements;
public class HooksDefinition
{
public List<string> OnServerStartup { get; set; } = new();
public List<string> OnSessionChange { get; set; } = new();
}
================================================
FILE: src/ES.SFTP/Configuration/Elements/HostKeysDefinition.cs
================================================
namespace ES.SFTP.Configuration.Elements;
public class HostKeysDefinition
{
public string Ed25519 { get; set; }
public string Rsa { get; set; }
}
================================================
FILE: src/ES.SFTP/Configuration/Elements/LoggingDefinition.cs
================================================
namespace ES.SFTP.Configuration.Elements;
public class LoggingDefinition
{
public bool IgnoreNoIdentificationString { get; set; }
}
================================================
FILE: src/ES.SFTP/Configuration/Elements/SftpConfiguration.cs
================================================
namespace ES.SFTP.Configuration.Elements;
public class SftpConfiguration
{
public GlobalConfiguration Global { get; set; } = new();
public List<UserDefinition> Users { get; set; } = new();
public List<GroupDefinition> Groups { get; set; } = new();
}
================================================
FILE: src/ES.SFTP/Configuration/Elements/UserDefinition.cs
================================================
namespace ES.SFTP.Configuration.Elements;
public class UserDefinition
{
public string Username { get; set; }
public string Password { get; set; }
public bool PasswordIsEncrypted { get; set; }
public List<string> AllowedHosts { get; set; } = new();
// ReSharper disable once InconsistentNaming
public int? UID { get; set; }
// ReSharper disable once InconsistentNaming
public int? GID { get; set; }
public ChrootDefinition Chroot { get; set; } = new();
public List<string> Directories { get; set; } = new();
public List<string> PublicKeys { get; set; } = new();
}
================================================
FILE: src/ES.SFTP/Dockerfile
================================================
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
RUN apt-get update && \
# - Install required packages
#
apt-get -y install members acl iputils-ping nano tini curl && \
#
# - Install openssh-server
apt-get -y install openssh-server && \
#
# - Install sssd
apt-get -y install sssd libpam-sss libnss-sss && \
#
# - Cleanup
rm -rf /var/lib/apt/lists/* && \
#
# - Create OpenSSH directory
mkdir -p /var/run/sshd && \
#
# - Remove default host keys
rm -f /etc/ssh/ssh_host_*key*
WORKDIR /app
EXPOSE 22 25080
FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim-amd64 AS build
WORKDIR /src
COPY ["ES.SFTP/ES.SFTP.csproj", "ES.SFTP/"]
RUN dotnet restore "ES.SFTP/ES.SFTP.csproj"
COPY . .
WORKDIR "/src/ES.SFTP"
RUN dotnet build "ES.SFTP.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "ES.SFTP.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["tini", "--", "dotnet", "ES.SFTP.dll"]
================================================
FILE: src/ES.SFTP/ES.SFTP.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileTag>emberstack/sftp:dev</DockerfileTag>
<DockerfileRunArguments>-p 2222:22 -p 25080:25080 --name sftpdev --privileged</DockerfileRunArguments>
<ServerGarbageCollection>false</ServerGarbageCollection>
</PropertyGroup>
<ItemGroup>
<None Remove="config\sssd.conf" />
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="app.logging.Development.json" />
<_ContentIncludedByDefault Remove="app.logging.json" />
<_ContentIncludedByDefault Remove="config\sftp.json" />
</ItemGroup>
<ItemGroup>
<Content Include="config\sssd.conf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="7.2.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="6.0.1" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.14.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="4.2.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.1.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.3.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
</ItemGroup>
<ItemGroup>
<Content Update="appsettings.Development.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
</Project>
================================================
FILE: src/ES.SFTP/Extensions/DirectoryInfoExtensions.cs
================================================
namespace ES.SFTP.Extensions;
public static class DirectoryInfoExtensions
{
public static bool IsDescendentOf(this DirectoryInfo directory, DirectoryInfo parent)
{
if (parent == null) return false;
if (directory.Parent == null) return false;
if (directory.Parent.FullName == parent.FullName) return true;
return directory.Parent.IsDescendentOf(parent);
}
}
================================================
FILE: src/ES.SFTP/Interop/ProcessRunOutput.cs
================================================
namespace ES.SFTP.Interop;
public class ProcessRunOutput
{
public string Output { get; set; }
public int ExitCode { get; set; }
}
================================================
FILE: src/ES.SFTP/Interop/ProcessUtil.cs
================================================
using System.Diagnostics;
using System.Text;
namespace ES.SFTP.Interop;
public class ProcessUtil
{
public static Task<ProcessRunOutput> QuickRun(string filename, string arguments = null,
bool throwOnError = true)
{
var outputStringBuilder = new StringBuilder();
var process = new Process
{
StartInfo =
{
FileName = filename,
Arguments = arguments ?? string.Empty,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};
process.OutputDataReceived += (_, e) => outputStringBuilder.Append(e.Data);
process.ErrorDataReceived += (_, e) => outputStringBuilder.Append(e.Data);
try
{
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
process.WaitForExit();
}
catch (Exception exception)
{
if (throwOnError) throw;
return Task.FromResult(new ProcessRunOutput
{
ExitCode = 1,
Output = exception.Message
});
}
var output = outputStringBuilder.ToString();
if (process.ExitCode != 0 && throwOnError)
throw new Exception(
$"Process failed with exit code '{process.ExitCode}.{Environment.NewLine}{output}'");
return Task.FromResult(new ProcessRunOutput
{
ExitCode = process.ExitCode,
Output = output
});
}
}
================================================
FILE: src/ES.SFTP/Messages/Configuration/SftpConfigurationRequest.cs
================================================
using ES.SFTP.Configuration.Elements;
using MediatR;
namespace ES.SFTP.Messages.Configuration;
public class SftpConfigurationRequest : IRequest<SftpConfiguration>
{
}
================================================
FILE: src/ES.SFTP/Messages/Events/ConfigurationChanged.cs
================================================
using MediatR;
namespace ES.SFTP.Messages.Events;
public class ConfigurationChanged : INotification
{
}
================================================
FILE: src/ES.SFTP/Messages/Events/ServerStartupEvent.cs
================================================
using MediatR;
namespace ES.SFTP.Messages.Events;
public class ServerStartupEvent : INotification
{
}
================================================
FILE: src/ES.SFTP/Messages/Events/UserSessionStartedEvent.cs
================================================
using MediatR;
namespace ES.SFTP.Messages.Events;
public class UserSessionChangedEvent : INotification
{
public string Username { get; set; }
public string SessionState { get; set; }
}
================================================
FILE: src/ES.SFTP/Messages/Pam/PamEventRequest.cs
================================================
using MediatR;
namespace ES.SFTP.Messages.Pam;
public class PamEventRequest : IRequest<bool>
{
public string Username { get; set; }
public string EventType { get; set; }
public string Service { get; set; }
}
================================================
FILE: src/ES.SFTP/Program.cs
================================================
using System.Reflection;
using Autofac;
using Autofac.Extensions.DependencyInjection;
using ES.SFTP.Configuration;
using ES.SFTP.Configuration.Elements;
using ES.SFTP.Security;
using ES.SFTP.SSH;
using MediatR;
using MediatR.Pipeline;
using Serilog;
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("app.logging.json")
.AddEnvironmentVariables(nameof(ES))
.AddCommandLine(args)
.Build())
.CreateLogger();
try
{
Log.Information("Starting host");
var builder = WebApplication.CreateBuilder(args);
builder.Environment.EnvironmentName =
Environment.GetEnvironmentVariable($"{nameof(ES)}_{nameof(Environment)}") ??
Environments.Production;
builder.Configuration.AddJsonFile("app.logging.json", false, false);
builder.Configuration.AddJsonFile("config/sftp.json", false, true);
builder.Configuration.AddEnvironmentVariables(nameof(ES));
builder.Configuration.AddCommandLine(args);
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
builder.Host.UseSerilog((hostingContext, loggerConfiguration) => loggerConfiguration
.ReadFrom.Configuration(hostingContext.Configuration)
.Enrich.FromLogContext(), true);
builder.Host.UseConsoleLifetime();
builder.Services.AddHttpClient();
builder.Services.AddOptions();
builder.Services.AddHealthChecks();
builder.Services.AddMediatR(typeof(void).Assembly);
builder.Services.AddControllers();
builder.Services.Configure<SftpConfiguration>(builder.Configuration);
builder.Host.ConfigureContainer((ContainerBuilder container) =>
{
container.RegisterAssemblyTypes(typeof(IMediator).GetTypeInfo().Assembly).AsImplementedInterfaces();
container.RegisterGeneric(typeof(RequestPostProcessorBehavior<,>)).As(typeof(IPipelineBehavior<,>));
container.RegisterGeneric(typeof(RequestPreProcessorBehavior<,>)).As(typeof(IPipelineBehavior<,>));
container.Register<ServiceFactory>(ctx =>
{
var c = ctx.Resolve<IComponentContext>();
return t => c.Resolve(t);
});
container.RegisterType<SessionHandler>().AsImplementedInterfaces().SingleInstance();
container.RegisterType<HookRunner>().AsImplementedInterfaces().SingleInstance();
container.RegisterType<ConfigurationService>().AsImplementedInterfaces().SingleInstance();
container.RegisterType<AuthenticationService>().AsImplementedInterfaces().SingleInstance();
container.RegisterType<UserManagementService>().AsImplementedInterfaces().SingleInstance();
container.RegisterType<SSHService>().AsImplementedInterfaces().SingleInstance();
});
builder.WebHost.ConfigureKestrel(options => { options.ListenLocalhost(25080); });
var app = builder.Build();
if (!app.Environment.IsDevelopment()) app.UseExceptionHandler("/Error");
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
await app.RunAsync();
return 0;
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly");
return 1;
}
finally
{
Log.CloseAndFlush();
}
================================================
FILE: src/ES.SFTP/Properties/launchSettings.json
================================================
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"HOST": {
"commandName": "Project",
"launchBrowser": false,
"environmentVariables": {
"SFTP_ENVIRONMENT": "Development"
},
"applicationUrl": "http://0.0.0.0:25080"
},
"Docker": {
"commandName": "Docker",
"launchBrowser": false,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
"publishAllPorts": true,
"environmentVariables": {
"SFTP_ENVIRONMENT": "Development"
},
"httpPort": 56895
}
}
}
================================================
FILE: src/ES.SFTP/SSH/Configuration/MatchBlock.cs
================================================
using System.Text;
namespace ES.SFTP.SSH.Configuration;
public class MatchBlock
{
public enum MatchCriteria
{
All,
User,
Group
}
public MatchCriteria Criteria { get; set; } = MatchCriteria.All;
public List<string> Match { get; set; } = new();
public List<string> Except { get; set; } = new();
public List<string> Declarations { get; set; } = new();
private string GetPatternLine()
{
var builder = new StringBuilder();
builder.Append($"Match {Criteria} ");
var patternList = (Match ?? new List<string>()).Where(s => !string.IsNullOrWhiteSpace(s))
.Select(s => $"{s.Trim()}").Distinct().ToList();
patternList.AddRange((Except ?? new List<string>()).Where(s => !string.IsNullOrWhiteSpace(s))
.Select(s => $"!{s.Trim()}").Distinct().ToList());
var exceptList = string.Join(",", patternList);
if (!string.IsNullOrWhiteSpace(exceptList)) builder.Append($"\"{exceptList}\"");
return builder.ToString();
}
public override string ToString()
{
var builder = new StringBuilder();
builder.AppendLine(GetPatternLine());
foreach (var declaration in (Declarations ?? new List<string>()).Where(declaration =>
!string.IsNullOrWhiteSpace(declaration)))
builder.AppendLine(declaration?.Trim());
return builder.ToString();
}
}
================================================
FILE: src/ES.SFTP/SSH/Configuration/SSHConfiguration.cs
================================================
using System.Text;
namespace ES.SFTP.SSH.Configuration;
public class SSHConfiguration
{
public List<MatchBlock> MatchBlocks { get; } = new();
public List<string> AllowUsers { get; } = new();
public string Ciphers { get; set; }
public string HostKeyAlgorithms { get; set; }
public string KexAlgorithms { get; set; }
public string MACs { get; set; }
public string PKIandPassword { get; set; }
public override string ToString()
{
var builder = new StringBuilder();
builder.AppendLine();
builder.AppendLine("UsePAM yes");
builder.AppendLine("# SSH Protocol");
builder.AppendLine("Protocol 2");
builder.AppendLine();
builder.AppendLine("# Host Keys");
builder.AppendLine("HostKey /etc/ssh/ssh_host_ed25519_key");
builder.AppendLine("HostKey /etc/ssh/ssh_host_rsa_key");
builder.AppendLine();
builder.AppendLine("# Cryptographic policy");
if (!string.IsNullOrWhiteSpace(Ciphers)) builder.AppendLine($"Ciphers {Ciphers}");
if (!string.IsNullOrWhiteSpace(HostKeyAlgorithms)) builder.AppendLine($"HostKeyAlgorithms {HostKeyAlgorithms}");
if (!string.IsNullOrWhiteSpace(KexAlgorithms)) builder.AppendLine($"KexAlgorithms {KexAlgorithms}");
if (!string.IsNullOrWhiteSpace(MACs)) builder.AppendLine($"MACs {MACs}");
builder.AppendLine();
builder.AppendLine("# Disable DNS for fast connections");
builder.AppendLine("UseDNS no");
builder.AppendLine();
builder.AppendLine("# Logging");
builder.AppendLine("LogLevel INFO");
builder.AppendLine();
builder.AppendLine("# Subsystem");
builder.AppendLine("Subsystem sftp internal-sftp");
builder.AppendLine();
builder.AppendLine("# Allowed users");
builder.AppendLine($"AllowUsers {string.Join(" ", AllowUsers)}");
builder.AppendLine();
if (PKIandPassword == "true") builder.AppendLine("AuthenticationMethods \"publickey,password\"");
builder.AppendLine();
builder.AppendLine("# Match blocks");
foreach (var matchBlock in MatchBlocks)
{
builder.Append(matchBlock);
builder.AppendLine();
}
return builder.ToString();
}
}
================================================
FILE: src/ES.SFTP/SSH/HookRunner.cs
================================================
using System.Diagnostics.CodeAnalysis;
using ES.SFTP.Interop;
using ES.SFTP.Messages.Configuration;
using ES.SFTP.Messages.Events;
using MediatR;
namespace ES.SFTP.SSH;
public class HookRunner : INotificationHandler<ServerStartupEvent>, INotificationHandler<UserSessionChangedEvent>
{
private readonly ILogger _logger;
private readonly IMediator _mediator;
public HookRunner(ILogger<HookRunner> logger, IMediator mediator)
{
_logger = logger;
_mediator = mediator;
}
[SuppressMessage("ReSharper", "MethodSupportsCancellation")]
public async Task Handle(ServerStartupEvent request, CancellationToken cancellationToken)
{
var sftpConfig = await _mediator.Send(new SftpConfigurationRequest());
var hooks = sftpConfig.Global.Hooks.OnServerStartup ?? new List<string>();
foreach (var hook in hooks) await RunHook(hook);
}
[SuppressMessage("ReSharper", "MethodSupportsCancellation")]
public async Task Handle(UserSessionChangedEvent request, CancellationToken cancellationToken)
{
var sftpConfig = await _mediator.Send(new SftpConfigurationRequest());
var hooks = sftpConfig.Global.Hooks.OnSessionChange ?? new List<string>();
var args = string.Join(' ', request.SessionState, request.Username);
foreach (var hook in hooks) await RunHook(hook, args);
}
private async Task RunHook(string hook, string args = null)
{
if (!File.Exists(hook))
{
_logger.LogInformation("Hook '{hook}' does not exist", hook);
return;
}
var execPermissionOutput = await ProcessUtil.QuickRun("bash",
$"-c \"if [[ -x {hook} ]]; then echo 'true'; else echo 'false'; fi\"", false);
if (execPermissionOutput.ExitCode != 0 ||
!bool.TryParse(execPermissionOutput.Output, out var isExecutable) ||
!isExecutable)
await ProcessUtil.QuickRun("chmod", $"+x {hook}");
_logger.LogDebug("Executing hook '{hook}'", hook);
var hookRun = await ProcessUtil.QuickRun(hook, args, false);
if (string.IsNullOrWhiteSpace(hookRun.Output))
_logger.LogDebug("Hook '{hook}' completed with exit code {exitCode}.", hook, hookRun.ExitCode);
else
_logger.LogDebug(
"Hook '{hook}' completed with exit code {exitCode}." +
$"{Environment.NewLine}Output:{Environment.NewLine}{{output}}",
hook, hookRun.ExitCode, hookRun.Output);
}
}
================================================
FILE: src/ES.SFTP/SSH/SSHService.cs
================================================
using System.Diagnostics;
using ES.SFTP.Configuration.Elements;
using ES.SFTP.Interop;
using ES.SFTP.Messages.Configuration;
using ES.SFTP.Messages.Events;
using ES.SFTP.SSH.Configuration;
using MediatR;
namespace ES.SFTP.SSH;
public class SSHService : IHostedService, INotificationHandler<ConfigurationChanged>
{
private const string SshDirPath = "/etc/ssh";
private static readonly string KeysImportDirPath = Path.Combine(SshDirPath, "keys");
private static readonly string ConfigFilePath = Path.Combine(SshDirPath, "sshd_config");
private readonly ILogger<SSHService> _logger;
private readonly IMediator _mediator;
private bool _loggingIgnoreNoIdentificationString;
private Process _serverProcess;
private Action _serviceProcessExitAction;
public SSHService(ILogger<SSHService> logger, IMediator mediator)
{
_logger = logger;
_mediator = mediator;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogDebug("Starting");
await RestartService(true);
_logger.LogInformation("Started");
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogDebug("Stopping");
await StopOpenSSH();
_logger.LogInformation("Stopped");
}
public async Task Handle(ConfigurationChanged notification, CancellationToken cancellationToken)
{
await RestartService();
}
private async Task RestartService(bool forceStop = false)
{
await StopOpenSSH(forceStop);
await UpdateHostKeyFiles();
await UpdateConfiguration();
await StartOpenSSH();
}
private async Task UpdateConfiguration()
{
var sftpConfig = await _mediator.Send(new SftpConfigurationRequest());
_loggingIgnoreNoIdentificationString = sftpConfig.Global.Logging.IgnoreNoIdentificationString;
var sshdConfig = new SSHConfiguration
{
Ciphers = sftpConfig.Global.Ciphers,
HostKeyAlgorithms = sftpConfig.Global.HostKeyAlgorithms,
KexAlgorithms = sftpConfig.Global.KexAlgorithms,
MACs = sftpConfig.Global.MACs,
PKIandPassword = sftpConfig.Global.PKIandPassword
};
var exceptionalUsers = sftpConfig.Users.Where(s => s.Chroot != null).ToList();
var standardDeclarations = new[]
{
"X11Forwarding no",
"AllowTcpForwarding no"
};
sshdConfig.AllowUsers.AddRange(sftpConfig.Users.Select(s =>
s.AllowedHosts.Any()
? $"{s.Username}@{string.Join(",", s.AllowedHosts)}"
: s.Username)
);
sshdConfig.MatchBlocks.AddRange(exceptionalUsers.Select(s => new MatchBlock
{
Criteria = MatchBlock.MatchCriteria.User,
Match = {s.Username},
Declarations = new List<string>(standardDeclarations)
{
$"ChrootDirectory {s.Chroot.Directory}",
!string.IsNullOrWhiteSpace(s.Chroot.StartPath)
? $"ForceCommand internal-sftp -d {s.Chroot.StartPath}"
: "ForceCommand internal-sftp"
}
}));
sshdConfig.MatchBlocks.Add(new MatchBlock
{
Criteria = MatchBlock.MatchCriteria.User,
Match = {"*"},
//Except = exceptionalUsers.Select(s => s.Username).ToList(),
Declarations = new List<string>(standardDeclarations)
{
$"ChrootDirectory {sftpConfig.Global.Chroot.Directory}",
!string.IsNullOrWhiteSpace(sftpConfig.Global.Chroot.StartPath)
? $"ForceCommand internal-sftp -d {sftpConfig.Global.Chroot.StartPath}"
: "ForceCommand internal-sftp"
}
});
var resultingConfig = sshdConfig.ToString();
await File.WriteAllTextAsync(ConfigFilePath, resultingConfig);
}
private async Task UpdateHostKeyFiles()
{
var config = await _mediator.Send(new SftpConfigurationRequest());
_logger.LogDebug("Updating host key files");
Directory.CreateDirectory(KeysImportDirPath);
var hostKeys = new[]
{
new
{
Type = nameof(HostKeysDefinition.Ed25519),
KeygenArgs = "-t ed25519 -f {0} -N \"\"",
File = "ssh_host_ed25519_key"
},
new
{
Type = nameof(HostKeysDefinition.Rsa),
KeygenArgs = "-t rsa -b 4096 -f {0} -N \"\"",
File = "ssh_host_rsa_key"
}
};
foreach (var hostKeyType in hostKeys)
{
var filePath = Path.Combine(KeysImportDirPath, hostKeyType.File);
if (File.Exists(filePath)) continue;
var configValue = (string) config.Global.HostKeys.GetType().GetProperty(hostKeyType.Type)
?.GetValue(config.Global.HostKeys, null);
if (!string.IsNullOrWhiteSpace(configValue))
{
_logger.LogDebug("Writing host key file '{file}' from config", filePath);
await File.WriteAllTextAsync(filePath, configValue);
}
else
{
_logger.LogDebug("Generating host key file '{file}'", filePath);
var keygenArgs = string.Format(hostKeyType.KeygenArgs, filePath);
await ProcessUtil.QuickRun("ssh-keygen", keygenArgs);
}
}
foreach (var file in Directory.GetFiles(KeysImportDirPath))
{
var targetFile = Path.Combine(SshDirPath, Path.GetFileName(file));
_logger.LogDebug("Copying '{sourceFile}' to '{targetFile}'", file, targetFile);
File.Copy(file, targetFile, true);
await ProcessUtil.QuickRun("chown", $"root:root \"{targetFile}\"");
await ProcessUtil.QuickRun("chmod", $"700 \"{targetFile}\"");
}
}
private async Task StartOpenSSH()
{
_logger.LogInformation("Starting 'sshd' process");
_serviceProcessExitAction = () =>
{
_logger.LogWarning("'sshd' process has stopped. Restarting process.");
RestartService().Wait();
};
void ListenForExit()
{
//Use this approach since the Exited event does not trigger on process crash
Task.Run(() =>
{
_serverProcess.WaitForExit();
_serviceProcessExitAction?.Invoke();
});
}
_serverProcess = new Process
{
StartInfo =
{
FileName = "/usr/sbin/sshd",
Arguments = "-D -e",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};
_serverProcess.OutputDataReceived -= OnSSHOutput;
_serverProcess.ErrorDataReceived -= OnSSHOutput;
_serverProcess.OutputDataReceived += OnSSHOutput;
_serverProcess.ErrorDataReceived += OnSSHOutput;
_serverProcess.Start();
ListenForExit();
_serverProcess.BeginOutputReadLine();
_serverProcess.BeginErrorReadLine();
await _mediator.Publish(new ServerStartupEvent());
}
private void OnSSHOutput(object sender, DataReceivedEventArgs e)
{
if (string.IsNullOrWhiteSpace(e.Data)) return;
if (_loggingIgnoreNoIdentificationString &&
e.Data.Trim().StartsWith("Did not receive identification string from")) return;
_logger.LogTrace($"sshd - {e.Data}");
}
private async Task StopOpenSSH(bool force = false)
{
if (_serverProcess != null)
{
_logger.LogDebug("Stopping 'sshd' process");
_serviceProcessExitAction = null;
_serverProcess.Kill(true);
_serverProcess.OutputDataReceived -= OnSSHOutput;
_serverProcess.ErrorDataReceived -= OnSSHOutput;
_logger.LogInformation("Stopped 'sshd' process");
_serverProcess.Dispose();
_serverProcess = null;
}
if (force)
{
var arguments = Debugger.IsAttached ? "-q sshd" : "-q -w sshd";
var command = await ProcessUtil.QuickRun("killall", arguments, false);
if (command.ExitCode != 0 && command.ExitCode != 1 && !string.IsNullOrWhiteSpace(command.Output))
throw new Exception(
$"Could not stop existing sshd processes.{Environment.NewLine}{command.Output}");
}
}
}
================================================
FILE: src/ES.SFTP/SSH/SessionHandler.cs
================================================
using System.Diagnostics.CodeAnalysis;
using ES.SFTP.Configuration.Elements;
using ES.SFTP.Extensions;
using ES.SFTP.Interop;
using ES.SFTP.Messages.Configuration;
using ES.SFTP.Messages.Events;
using ES.SFTP.Messages.Pam;
using MediatR;
namespace ES.SFTP.SSH;
public class SessionHandler : IRequestHandler<PamEventRequest, bool>
{
private const string HomeBasePath = "/home";
private const string SftpUserInventoryGroup = "sftp-user-inventory";
private readonly ILogger _logger;
private readonly IMediator _mediator;
private SftpConfiguration _config;
public SessionHandler(ILogger<SessionHandler> logger, IMediator mediator)
{
_logger = logger;
_mediator = mediator;
}
[SuppressMessage("ReSharper", "MethodSupportsCancellation")]
public async Task<bool> Handle(PamEventRequest request, CancellationToken cancellationToken)
{
switch (request.EventType)
{
case "open_session":
await PrepareUserForSftp(request.Username);
break;
}
await _mediator.Publish(new UserSessionChangedEvent
{
Username = request.Username,
SessionState = request.EventType
});
return true;
}
private async Task PrepareUserForSftp(string username)
{
_logger.LogDebug("Configuring session for user '{user}'", username);
_config = await _mediator.Send(new SftpConfigurationRequest());
var user = _config.Users.FirstOrDefault(s => s.Username == username) ?? new UserDefinition
{
Username = username,
Chroot = _config.Global.Chroot,
Directories = _config.Global.Directories
};
var homeDirPath = Path.Combine(HomeBasePath, username);
var chroot = user.Chroot ?? _config.Global.Chroot;
//Parse chroot path by replacing markers
var chrootPath = string.Join("%%h",
chroot.Directory.Split("%%h")
.Select(s => s.Replace("%h", homeDirPath)).ToList());
chrootPath = string.Join("%%u",
chrootPath.Split("%%u")
.Select(s => s.Replace("%u", username)).ToList());
//Create chroot directory and set owner to root and correct permissions
var chrootDirectory = Directory.CreateDirectory(chrootPath);
await ProcessUtil.QuickRun("chown", $"root:root {chrootDirectory.FullName}");
await ProcessUtil.QuickRun("chmod", $"755 {chrootDirectory.FullName}");
var directories = new List<string>();
directories.AddRange(_config.Global.Directories);
directories.AddRange(user.Directories);
foreach (var directory in directories.Distinct().OrderBy(s => s).ToList())
{
var dirInfo = new DirectoryInfo(Path.Combine(chrootDirectory.FullName, directory));
if (!dirInfo.Exists)
{
_logger.LogDebug("Creating directory '{dir}' for user '{user}'", dirInfo.FullName, username);
Directory.CreateDirectory(dirInfo.FullName);
}
try
{
if (dirInfo.IsDescendentOf(chrootDirectory))
{
//Set the user as owner for directory and all parents until chroot path
var dir = dirInfo;
while (dir.FullName != chrootDirectory.FullName)
{
await ProcessUtil.QuickRun("chown", $"{username}:{SftpUserInventoryGroup} {dir.FullName}");
dir = dir.Parent ?? chrootDirectory;
}
}
else
{
_logger.LogWarning(
"Directory '{dir}' is not within chroot path '{chroot}'. Setting direct permissions.",
dirInfo.FullName, chrootDirectory.FullName);
await ProcessUtil.QuickRun("chown",
$"{username}:{SftpUserInventoryGroup} {dirInfo.FullName}");
}
}
catch (Exception exception)
{
_logger.LogWarning(exception, "Exception occured while setting permissions for '{dir}' ",
dirInfo.FullName);
}
}
_logger.LogInformation("Session ready for user '{user}'", username);
}
}
================================================
FILE: src/ES.SFTP/Security/AuthenticationService.cs
================================================
using System.Text;
using ES.SFTP.Interop;
namespace ES.SFTP.Security;
public class AuthenticationService : IHostedService
{
private const string PamDirPath = "/etc/pam.d";
private const string PamHookName = "sftp-hook";
private readonly ILogger _logger;
public AuthenticationService(ILogger<AuthenticationService> logger)
{
_logger = logger;
} // ReSharper disable MethodSupportsCancellation
public async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogDebug("Starting");
var pamCommonSessionFile = Path.Combine(PamDirPath, "common-session");
var pamSftpHookFile = Path.Combine(PamDirPath, PamHookName);
_logger.LogDebug("Stopping SSSD service");
await ProcessUtil.QuickRun("service", "sssd stop", false);
_logger.LogDebug("Applying SSSD configuration");
File.Copy("./config/sssd.conf", "/etc/sssd/sssd.conf", true);
await ProcessUtil.QuickRun("chown", "root:root \"/etc/sssd/sssd.conf\"");
await ProcessUtil.QuickRun("chmod", "600 \"/etc/sssd/sssd.conf\"");
_logger.LogDebug("Installing PAM hook");
var scriptsDirectory = Path.Combine(PamDirPath, "scripts");
if (!Directory.Exists(scriptsDirectory)) Directory.CreateDirectory(scriptsDirectory);
var hookScriptFile = Path.Combine(new DirectoryInfo(scriptsDirectory).FullName, "sftp-pam-event.sh");
var eventsScriptBuilder = new StringBuilder();
eventsScriptBuilder.AppendLine("#!/bin/sh");
eventsScriptBuilder.AppendLine(
"curl \"http://localhost:25080/api/events/pam/generic?username=$PAM_USER&type=$PAM_TYPE&service=$PAM_SERVICE\"");
await File.WriteAllTextAsync(hookScriptFile, eventsScriptBuilder.ToString());
await ProcessUtil.QuickRun("chown", $"root:root \"{hookScriptFile}\"");
await ProcessUtil.QuickRun("chmod", $"+x \"{hookScriptFile}\"");
var hookBuilder = new StringBuilder();
hookBuilder.AppendLine("# This file is used to signal the SFTP service on user events.");
hookBuilder.AppendLine($"session required pam_exec.so {new FileInfo(hookScriptFile).FullName}");
await File.WriteAllTextAsync(pamSftpHookFile, hookBuilder.ToString());
await ProcessUtil.QuickRun("chown", $"root:root \"{pamSftpHookFile}\"");
await ProcessUtil.QuickRun("chmod", $"644 \"{pamSftpHookFile}\"");
if (!(await File.ReadAllTextAsync(pamCommonSessionFile)).Contains($"@include {PamHookName}"))
await File.AppendAllTextAsync(pamCommonSessionFile, $"@include {PamHookName}{Environment.NewLine}");
_logger.LogDebug("Restarting SSSD service");
await ProcessUtil.QuickRun("service", "sssd restart", false);
_logger.LogInformation("Started");
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogDebug("Stopping");
await ProcessUtil.QuickRun("service", "sssd stop", false);
_logger.LogInformation("Stopped");
}
}
================================================
FILE: src/ES.SFTP/Security/GroupUtil.cs
================================================
using ES.SFTP.Interop;
namespace ES.SFTP.Security;
public class GroupUtil
{
public static async Task<bool> GroupExists(string groupNameOrId)
{
var command = await ProcessUtil.QuickRun("getent", $"group {groupNameOrId}", false);
return command.ExitCode == 0 && !string.IsNullOrWhiteSpace(command.Output);
}
public static async Task GroupCreate(string name, bool force = false, int? groupId = null,
bool nonUniqueGroupId = true)
{
await ProcessUtil.QuickRun("groupadd",
$"{(force ? "-f" : string.Empty)} {(groupId != null ? $"-g {groupId} {(nonUniqueGroupId ? "-o" : string.Empty)}" : string.Empty)} {name}");
}
public static async Task GroupAddUser(string group, string username)
{
await ProcessUtil.QuickRun("usermod", $"-a -G {group} {username}");
}
public static async Task GroupRemoveUser(string group, string username)
{
await ProcessUtil.QuickRun("usermod", $"-G {group} {username}");
}
public static async Task<IReadOnlyList<string>> GroupListUsers(string group)
{
var command = await ProcessUtil.QuickRun("members", group, false);
if (command.ExitCode != 0 && command.ExitCode != 1 && !string.IsNullOrWhiteSpace(command.Output))
throw new Exception($"Get group members command failed with exit code {command.ExitCode} and message:" +
$"{Environment.NewLine}{command.Output}");
return command.Output.Split(' ', StringSplitOptions.RemoveEmptyEntries).OrderBy(s => s).ToList();
}
public static async Task<int> GroupGetId(string groupNameOrId)
{
var command = await ProcessUtil.QuickRun("getent", $"group {groupNameOrId}");
var groupEntryValues = command.Output.Split(":");
return int.Parse(groupEntryValues[2]);
}
public static async Task GroupSetId(string groupNameOrId, int id)
{
await ProcessUtil.QuickRun("groupmod", $"-g {id} {groupNameOrId}");
}
}
================================================
FILE: src/ES.SFTP/Security/UserManagementService.cs
================================================
using System.Diagnostics.CodeAnalysis;
using System.Text;
using ES.SFTP.Interop;
using ES.SFTP.Messages.Configuration;
using ES.SFTP.Messages.Events;
using MediatR;
namespace ES.SFTP.Security;
public class UserManagementService : IHostedService, INotificationHandler<ConfigurationChanged>
{
private const string HomeBasePath = "/home";
private const string SftpUserInventoryGroup = "sftp-user-inventory";
private readonly ILogger _logger;
private readonly IMediator _mediator;
public UserManagementService(ILogger<UserManagementService> logger, IMediator mediator)
{
_logger = logger;
_mediator = mediator;
}
[SuppressMessage("ReSharper", "MethodSupportsCancellation")]
public async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogDebug("Starting");
_logger.LogDebug("Ensuring '{home}' directory exists and has correct permissions", HomeBasePath);
Directory.CreateDirectory(HomeBasePath);
await ProcessUtil.QuickRun("chown", $"root:root \"{HomeBasePath}\"");
_logger.LogDebug("Ensuring group '{group}' exists", SftpUserInventoryGroup);
if (!await GroupUtil.GroupExists(SftpUserInventoryGroup))
{
_logger.LogInformation("Creating group '{group}'", SftpUserInventoryGroup);
await GroupUtil.GroupCreate(SftpUserInventoryGroup, true);
}
await SyncUsersAndGroups();
_logger.LogInformation("Started");
}
[SuppressMessage("ReSharper", "MethodSupportsCancellation")]
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogDebug("Stopping");
_logger.LogInformation("Stopped");
return Task.CompletedTask;
}
public async Task Handle(ConfigurationChanged notification, CancellationToken cancellationToken)
{
await SyncUsersAndGroups();
}
private async Task SyncUsersAndGroups()
{
var config = await _mediator.Send(new SftpConfigurationRequest());
_logger.LogInformation("Synchronizing users and groups");
//Remove users that do not exist in config anymore
var existingUsers = await GroupUtil.GroupListUsers(SftpUserInventoryGroup);
var toRemove = existingUsers.Where(s => !config.Users.Select(t => t.Username).Contains(s)).ToList();
foreach (var user in toRemove)
{
_logger.LogDebug("Removing user '{user}'", user, SftpUserInventoryGroup);
await UserUtil.UserDelete(user, false);
}
//Create groups as specified by the GID value for each user
foreach (var user in config.Users)
{
if (user.GID.HasValue)
{
_logger.LogInformation("Processing GID for user '{user}'", user.Username);
var virtualGroup = $"sftp-gid-{user.GID.Value}";
if (!await GroupUtil.GroupExists(virtualGroup))
{
_logger.LogDebug("Creating group '{group}' with GID '{gid}'", virtualGroup, user.GID.Value);
await GroupUtil.GroupCreate(virtualGroup, true, user.GID.Value);
}
}
}
foreach (var user in config.Users)
{
_logger.LogInformation("Processing user '{user}'", user.Username);
if (!await UserUtil.UserExists(user.Username))
{
_logger.LogDebug("Creating user '{user}'", user.Username);
await UserUtil.UserCreate(user.Username, true, user.GID);
_logger.LogDebug("Adding user '{user}' to '{group}'", user.Username, SftpUserInventoryGroup);
await GroupUtil.GroupAddUser(SftpUserInventoryGroup, user.Username);
}
_logger.LogDebug("Updating the password for user '{user}'", user.Username);
await UserUtil.UserSetPassword(user.Username, user.Password, user.PasswordIsEncrypted);
if (user.UID.HasValue && await UserUtil.UserGetId(user.Username) != user.UID.Value)
{
_logger.LogDebug("Updating the UID for user '{user}'", user.Username);
await UserUtil.UserSetId(user.Username, user.UID.Value);
}
var homeDir = Directory.CreateDirectory(Path.Combine(HomeBasePath, user.Username));
await ProcessUtil.QuickRun("chown", $"root:root {homeDir.FullName}");
await ProcessUtil.QuickRun("chmod", $"711 {homeDir.FullName}");
var sshDir = Directory.CreateDirectory(Path.Combine(homeDir.FullName, ".ssh"));
var sshKeysDir = Directory.CreateDirectory(Path.Combine(sshDir.FullName, "keys"));
var sshAuthKeysPath = Path.Combine(sshDir.FullName, "authorized_keys");
if (File.Exists(sshAuthKeysPath)) File.Delete(sshAuthKeysPath);
var authKeysBuilder = new StringBuilder();
foreach (var file in Directory.GetFiles(sshKeysDir.FullName))
{
_logger.LogDebug("Adding public key '{file}' for user '{user}'", file, user.Username);
authKeysBuilder.AppendLine(await File.ReadAllTextAsync(file));
}
foreach (var publicKey in user.PublicKeys)
{
_logger.LogDebug("Adding public key from config for user '{user}'", user.Username);
authKeysBuilder.AppendLine(publicKey);
}
await File.WriteAllTextAsync(sshAuthKeysPath, authKeysBuilder.ToString());
await ProcessUtil.QuickRun("chown", $"{user.Username} {sshAuthKeysPath}");
await ProcessUtil.QuickRun("chmod", $"400 {sshAuthKeysPath}");
}
foreach (var groupDefinition in config.Groups)
{
_logger.LogInformation("Processing group '{group}'", groupDefinition.Name);
var groupUsers = groupDefinition.Users ?? new List<string>();
if (!await GroupUtil.GroupExists(groupDefinition.Name))
{
_logger.LogDebug("Creating group '{group}' with GID '{gid}'", groupDefinition.Name,
groupDefinition.GID);
await GroupUtil.GroupCreate(groupDefinition.Name, true, groupDefinition.GID);
}
if (groupDefinition.GID.HasValue)
{
var currentId = await GroupUtil.GroupGetId(groupDefinition.Name);
if (currentId != groupDefinition.GID.Value)
{
_logger.LogDebug("Updating group '{group}' with GID '{gid}'", groupDefinition.Name,
groupDefinition.GID);
await GroupUtil.GroupSetId(groupDefinition.Name, groupDefinition.GID.Value);
}
}
var members = await GroupUtil.GroupListUsers(groupDefinition.Name);
var toAdd = groupUsers.Where(s => !members.Contains(s)).ToList();
foreach (var user in toAdd)
{
if (!await UserUtil.UserExists(user)) continue;
_logger.LogDebug("Adding user '{user}' to '{group}'", user, groupDefinition.Name);
await GroupUtil.GroupAddUser(groupDefinition.Name, user);
}
members = await GroupUtil.GroupListUsers(groupDefinition.Name);
var usersToRemove = members.Where(s => !groupUsers.Contains(s)).ToList();
foreach (var user in usersToRemove)
{
_logger.LogDebug("Removing user '{user}'", user, groupDefinition.Name);
await GroupUtil.GroupRemoveUser(groupDefinition.Name, user);
}
}
}
}
================================================
FILE: src/ES.SFTP/Security/UserUtil.cs
================================================
using ES.SFTP.Interop;
namespace ES.SFTP.Security;
public class UserUtil
{
public static async Task<bool> UserExists(string username)
{
var command = await ProcessUtil.QuickRun("getent", $"passwd {username}", false);
return command.ExitCode == 0 && !string.IsNullOrWhiteSpace(command.Output);
}
public static async Task UserCreate(string username, bool noLoginShell = false, int? gid = null)
{
await ProcessUtil.QuickRun("useradd",
$"--comment {username} {(noLoginShell ? "-s /usr/sbin/nologin " : string.Empty)}{(gid.HasValue ? "-g " + gid.Value + " " : string.Empty)}{username}");
}
public static async Task UserDelete(string username, bool throwOnError = true)
{
await ProcessUtil.QuickRun("userdel", username, throwOnError);
}
public static async Task UserSetId(string username, int id, bool nonUnique = true)
{
await ProcessUtil.QuickRun("pkill", $"-U {await UserGetId(username)}", false);
await ProcessUtil.QuickRun("usermod",
$"{(nonUnique ? "--non-unique" : string.Empty)} --uid {id} {username}");
}
public static async Task UserSetPassword(string username, string password, bool passwordIsEncrypted)
{
if (string.IsNullOrEmpty(password))
await ProcessUtil.QuickRun("usermod", $"-p \"*\" {username}");
else
await ProcessUtil.QuickRun("bash",
$"-c \"echo '{username}:{password}' | chpasswd {(passwordIsEncrypted ? "-e" : string.Empty)}\"");
}
public static async Task<int> UserGetId(string username)
{
var command = await ProcessUtil.QuickRun("id", $"-u {username}");
return int.Parse(command.Output);
}
}
================================================
FILE: src/ES.SFTP/app.logging.Development.json
================================================
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}
================================================
FILE: src/ES.SFTP/app.logging.json
================================================
{
"Serilog": {
"Using": ["Serilog.Sinks.Console"],
"MinimumLevel": {
"Default": "Verbose",
"Override": {
"Microsoft.AspNetCore.Server.Kestrel": "Error",
"Microsoft": "Warning",
"System": "Warning",
"Microsoft.Extensions.Http": "Warning"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate":
"{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}"
}
}
]
}
}
================================================
FILE: src/ES.SFTP/appsettings.Development.json
================================================
{
}
================================================
FILE: src/ES.SFTP/appsettings.json
================================================
{
"AllowedHosts": "*"
}
================================================
FILE: src/ES.SFTP/config/sftp.json
================================================
{
"Global": {
"Chroot": {
"Directory": "%h",
"StartPath": "sftp"
},
"Directories": ["sftp"],
"Logging": {
"IgnoreNoIdentificationString": true
},
"Hooks": {
"OnServerStartup": [],
"OnSessionChange": []
}
},
"Users": [
{
"Username": "demo",
"Password": "demo"
}
],
"Groups": [
{
"Name": "demogroup",
"Users": ["demo"],
"GID": 5000
}
]
}
================================================
FILE: src/ES.SFTP/config/sssd.conf
================================================
[sssd]
config_file_version = 2
services = nss, pam
debug_level = 5
[pam]
[nss]
fallback_homedir = /home/%u
default_shell = /usr/sbin/nologin
================================================
FILE: src/ES.SFTP.sln
================================================
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31710.8
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ES.SFTP", "ES.SFTP\ES.SFTP.csproj", "{AA23AF3B-923C-4E65-BC4C-4E5663E29B1F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{AA23AF3B-923C-4E65-BC4C-4E5663E29B1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AA23AF3B-923C-4E65-BC4C-4E5663E29B1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AA23AF3B-923C-4E65-BC4C-4E5663E29B1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AA23AF3B-923C-4E65-BC4C-4E5663E29B1F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A4012BFD-4BA0-416A-BFC2-4F7DEF32362B}
EndGlobalSection
EndGlobal
================================================
FILE: src/ES.SFTP.sln.DotSettings
================================================
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SFTP/@EntryIndexedValue">SFTP</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SSH/@EntryIndexedValue">SSH</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SSHD/@EntryIndexedValue">SSHD</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SSSD/@EntryIndexedValue">SSSD</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=chown/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Chroot/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=killall/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=l_0022_002C_0020_0022_002Dq/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=sshd/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=sssd/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
================================================
FILE: src/docker-compose/docker-compose.override.dev.yaml
================================================
version: '3'
services:
sftp:
image: "emberstack/sftp:dev"
build:
context: ../
dockerfile: ES.SFTP.Host/Dockerfile
ports:
- "2222:22"
volumes:
- ../samples/sample.dev.sftp.json:/app/config/sftp.json:ro
- ../samples/.ssh/id_demo2_rsa.pub:/home/demo2/.ssh/keys/id_rsa.pub:ro
- ../samples/.ssh/id_demo2_ed25519.pub:/home/demo2/.ssh/keys/id_ed25519.pub:ro
================================================
FILE: src/docker-compose/docker-compose.yaml
================================================
version: '3'
services:
sftp:
image: "emberstack/sftp"
ports:
- "22:22"
volumes:
- ../samples/sample.sftp.json:/app/config/sftp.json:ro
================================================
FILE: src/helm/sftp/.helmignore
================================================
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
================================================
FILE: src/helm/sftp/Chart.yaml
================================================
apiVersion: v2
name: sftp
description: A Helm chart to deploy SFTP
type: application
version: 0.1.0
appVersion: 0.1.0
icon: https://raw.githubusercontent.com/emberstack/CDN/main/projects/docker-sftp/openssh.png
keywords:
- sftp
- openssh
- files
- storage
- ftp
home: https://github.com/EmberStack/docker-sftp
sources:
- https://github.com/EmberStack/docker-sftp
maintainers:
- name: winromulus
email: helm-charts@emberstack.com
================================================
FILE: src/helm/sftp/LICENSE
================================================
MIT License
Copyright (c) 2019 emberstack
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: src/helm/sftp/templates/NOTES.txt
================================================
SFTP can now be used for secure file transfers. You can connect to your service using any SFTP client.
================================================
FILE: src/helm/sftp/templates/_helpers.tpl
================================================
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "sftp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "sftp.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "sftp.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Common labels
*/}}
{{- define "sftp.labels" -}}
helm.sh/chart: {{ include "sftp.chart" . }}
{{ include "sftp.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{/*
Selector labels
*/}}
{{- define "sftp.selectorLabels" -}}
app.kubernetes.io/name: {{ include "sftp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{/*
Create the name of the service account to use
*/}}
{{- define "sftp.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{ default (include "sftp.fullname" .) .Values.serviceAccount.name }}
{{- else -}}
{{ default "default" .Values.serviceAccount.name }}
{{- end -}}
{{- end -}}
================================================
FILE: src/helm/sftp/templates/config-secret.yaml
================================================
{{- if .Values.configuration }}
apiVersion: v1
kind: Secret
metadata:
name: {{ template "sftp.fullname" . }}
labels:
{{- include "sftp.labels" . | nindent 4 }}
type: Opaque
stringData:
sftp.json: |-
{{ .Values.configuration | toPrettyJson | nindent 4 }}
{{- end }}
================================================
FILE: src/helm/sftp/templates/deployment.yaml
================================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "sftp.fullname" . }}
labels:
{{- include "sftp.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "sftp.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "sftp.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "sftp.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
{{- with .Values.initContainers }}
initContainers:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
{{- if .Values.image.tag }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
{{- else }}
image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}"
{{- end }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: ssh
containerPort: 22
protocol: TCP
{{- if or .Values.configuration .Values.storage.volumeMounts }}
volumeMounts:
{{- if .Values.configuration }}
- name: sftp-json
mountPath: "/app/config/sftp.json"
subPath: sftp.json
readOnly: true
{{- end }}
{{- with .Values.storage.volumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
livenessProbe:
tcpSocket:
port: ssh
readinessProbe:
tcpSocket:
port: ssh
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- if or .Values.configuration .Values.storage.volumes }}
volumes:
{{- if .Values.configuration }}
- name: sftp-json
secret:
secretName: {{ include "sftp.fullname" . }}
items:
- key: sftp.json
path: sftp.json
{{- end }}
{{- with .Values.storage.volumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
================================================
FILE: src/helm/sftp/templates/service.yaml
================================================
apiVersion: v1
kind: Service
metadata:
name: {{ include "sftp.fullname" . }}
labels:
{{- include "sftp.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if ne .Values.service.type "NodePort" }}
{{- if .Values.service.clusterIP }}
clusterIP: "{{ .Values.service.clusterIP }}"
{{- end }}
{{- end }}
{{- if .Values.service.externalIPs }}
externalIPs:
{{ toYaml .Values.service.externalIPs | indent 4 }}
{{- end }}
{{- if .Values.service.loadBalancerIP }}
loadBalancerIP: "{{ .Values.service.loadBalancerIP }}"
{{- end }}
{{- if .Values.service.loadBalancerSourceRanges }}
loadBalancerSourceRanges:
{{ toYaml .Values.service.loadBalancerSourceRanges | indent 4 }}
{{- end }}
{{- if and (semverCompare ">=1.7-0" .Capabilities.KubeVersion.GitVersion) (.Values.service.externalTrafficPolicy) }}
externalTrafficPolicy: "{{ .Values.service.externalTrafficPolicy }}"
{{- end }}
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: ssh
protocol: TCP
name: ssh
selector:
{{- include "sftp.selectorLabels" . | nindent 4 }}
================================================
FILE: src/helm/sftp/templates/serviceaccount.yaml
================================================
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "sftp.serviceAccountName" . }}
labels:
{{- include "sftp.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end -}}
================================================
FILE: src/helm/sftp/values.yaml
================================================
# Default values for sftp.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: emberstack/sftp
tag: ""
pullPolicy: Always
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
configuration: null
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name:
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
storage:
volumeMounts: []
volumes: []
initContainers: []
service:
type: ClusterIP
port: 22
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
nodeSelector: {}
tolerations: []
affinity: {}
gitextract_0tjh4w4i/
├── .dockerignore
├── .github/
│ ├── dependabot.yml
│ ├── stale.yml
│ └── workflows/
│ └── pipeline.yaml
├── .gitignore
├── LICENSE
├── README.md
├── samples/
│ ├── .ssh/
│ │ ├── id_demo2_ed25519
│ │ ├── id_demo2_ed25519.pub
│ │ ├── id_demo2_rsa
│ │ ├── id_demo2_rsa.pub
│ │ ├── id_demo_rsa
│ │ └── id_demo_rsa.pub
│ ├── hooks/
│ │ ├── onsessionchange
│ │ └── onstartup
│ ├── sample.dev.sftp.json
│ └── sample.sftp.json
└── src/
├── .dockerignore
├── ES.SFTP/
│ ├── Api/
│ │ └── PamEventsController.cs
│ ├── Configuration/
│ │ ├── ConfigurationService.cs
│ │ └── Elements/
│ │ ├── ChrootDefinition.cs
│ │ ├── GlobalConfiguration.cs
│ │ ├── GroupDefinition.cs
│ │ ├── HooksDefinition.cs
│ │ ├── HostKeysDefinition.cs
│ │ ├── LoggingDefinition.cs
│ │ ├── SftpConfiguration.cs
│ │ └── UserDefinition.cs
│ ├── Dockerfile
│ ├── ES.SFTP.csproj
│ ├── Extensions/
│ │ └── DirectoryInfoExtensions.cs
│ ├── Interop/
│ │ ├── ProcessRunOutput.cs
│ │ └── ProcessUtil.cs
│ ├── Messages/
│ │ ├── Configuration/
│ │ │ └── SftpConfigurationRequest.cs
│ │ ├── Events/
│ │ │ ├── ConfigurationChanged.cs
│ │ │ ├── ServerStartupEvent.cs
│ │ │ └── UserSessionStartedEvent.cs
│ │ └── Pam/
│ │ └── PamEventRequest.cs
│ ├── Program.cs
│ ├── Properties/
│ │ └── launchSettings.json
│ ├── SSH/
│ │ ├── Configuration/
│ │ │ ├── MatchBlock.cs
│ │ │ └── SSHConfiguration.cs
│ │ ├── HookRunner.cs
│ │ ├── SSHService.cs
│ │ └── SessionHandler.cs
│ ├── Security/
│ │ ├── AuthenticationService.cs
│ │ ├── GroupUtil.cs
│ │ ├── UserManagementService.cs
│ │ └── UserUtil.cs
│ ├── app.logging.Development.json
│ ├── app.logging.json
│ ├── appsettings.Development.json
│ ├── appsettings.json
│ └── config/
│ ├── sftp.json
│ └── sssd.conf
├── ES.SFTP.sln
├── ES.SFTP.sln.DotSettings
├── docker-compose/
│ ├── docker-compose.override.dev.yaml
│ └── docker-compose.yaml
└── helm/
└── sftp/
├── .helmignore
├── Chart.yaml
├── LICENSE
├── templates/
│ ├── NOTES.txt
│ ├── _helpers.tpl
│ ├── config-secret.yaml
│ ├── deployment.yaml
│ ├── service.yaml
│ └── serviceaccount.yaml
└── values.yaml
SYMBOL INDEX (79 symbols across 27 files)
FILE: src/ES.SFTP/Api/PamEventsController.cs
class PamEventsController (line 7) | [Route("api/events/pam")]
method PamEventsController (line 13) | public PamEventsController(ILogger<PamEventsController> logger, IMedia...
method OnGenericPamEvent (line 20) | [HttpGet]
FILE: src/ES.SFTP/Configuration/ConfigurationService.cs
class ConfigurationService (line 9) | public class ConfigurationService : IHostedService, IRequestHandler<Sftp...
method ConfigurationService (line 18) | public ConfigurationService(ILogger<ConfigurationService> logger,
method StartAsync (line 28) | public async Task StartAsync(CancellationToken cancellationToken)
method StopAsync (line 37) | public Task StopAsync(CancellationToken cancellationToken)
method Handle (line 48) | public Task<SftpConfiguration> Handle(SftpConfigurationRequest request...
method OnSftpConfigurationChanged (line 53) | private void OnSftpConfigurationChanged(SftpConfiguration arg1, string...
method UpdateConfiguration (line 60) | private Task UpdateConfiguration()
FILE: src/ES.SFTP/Configuration/Elements/ChrootDefinition.cs
class ChrootDefinition (line 3) | public class ChrootDefinition
FILE: src/ES.SFTP/Configuration/Elements/GlobalConfiguration.cs
class GlobalConfiguration (line 3) | public class GlobalConfiguration
FILE: src/ES.SFTP/Configuration/Elements/GroupDefinition.cs
class GroupDefinition (line 3) | public class GroupDefinition
FILE: src/ES.SFTP/Configuration/Elements/HooksDefinition.cs
class HooksDefinition (line 3) | public class HooksDefinition
FILE: src/ES.SFTP/Configuration/Elements/HostKeysDefinition.cs
class HostKeysDefinition (line 3) | public class HostKeysDefinition
FILE: src/ES.SFTP/Configuration/Elements/LoggingDefinition.cs
class LoggingDefinition (line 3) | public class LoggingDefinition
FILE: src/ES.SFTP/Configuration/Elements/SftpConfiguration.cs
class SftpConfiguration (line 3) | public class SftpConfiguration
FILE: src/ES.SFTP/Configuration/Elements/UserDefinition.cs
class UserDefinition (line 3) | public class UserDefinition
FILE: src/ES.SFTP/Extensions/DirectoryInfoExtensions.cs
class DirectoryInfoExtensions (line 3) | public static class DirectoryInfoExtensions
method IsDescendentOf (line 5) | public static bool IsDescendentOf(this DirectoryInfo directory, Direct...
FILE: src/ES.SFTP/Interop/ProcessRunOutput.cs
class ProcessRunOutput (line 3) | public class ProcessRunOutput
FILE: src/ES.SFTP/Interop/ProcessUtil.cs
class ProcessUtil (line 6) | public class ProcessUtil
method QuickRun (line 8) | public static Task<ProcessRunOutput> QuickRun(string filename, string ...
FILE: src/ES.SFTP/Messages/Configuration/SftpConfigurationRequest.cs
class SftpConfigurationRequest (line 6) | public class SftpConfigurationRequest : IRequest<SftpConfiguration>
FILE: src/ES.SFTP/Messages/Events/ConfigurationChanged.cs
class ConfigurationChanged (line 5) | public class ConfigurationChanged : INotification
FILE: src/ES.SFTP/Messages/Events/ServerStartupEvent.cs
class ServerStartupEvent (line 5) | public class ServerStartupEvent : INotification
FILE: src/ES.SFTP/Messages/Events/UserSessionStartedEvent.cs
class UserSessionChangedEvent (line 5) | public class UserSessionChangedEvent : INotification
FILE: src/ES.SFTP/Messages/Pam/PamEventRequest.cs
class PamEventRequest (line 5) | public class PamEventRequest : IRequest<bool>
FILE: src/ES.SFTP/SSH/Configuration/MatchBlock.cs
class MatchBlock (line 5) | public class MatchBlock
type MatchCriteria (line 7) | public enum MatchCriteria
method GetPatternLine (line 20) | private string GetPatternLine()
method ToString (line 33) | public override string ToString()
FILE: src/ES.SFTP/SSH/Configuration/SSHConfiguration.cs
class SSHConfiguration (line 5) | public class SSHConfiguration
method ToString (line 17) | public override string ToString()
FILE: src/ES.SFTP/SSH/HookRunner.cs
class HookRunner (line 9) | public class HookRunner : INotificationHandler<ServerStartupEvent>, INot...
method HookRunner (line 14) | public HookRunner(ILogger<HookRunner> logger, IMediator mediator)
method Handle (line 21) | [SuppressMessage("ReSharper", "MethodSupportsCancellation")]
method Handle (line 30) | [SuppressMessage("ReSharper", "MethodSupportsCancellation")]
method RunHook (line 39) | private async Task RunHook(string hook, string args = null)
FILE: src/ES.SFTP/SSH/SSHService.cs
class SSHService (line 11) | public class SSHService : IHostedService, INotificationHandler<Configura...
method SSHService (line 23) | public SSHService(ILogger<SSHService> logger, IMediator mediator)
method StartAsync (line 29) | public async Task StartAsync(CancellationToken cancellationToken)
method StopAsync (line 36) | public async Task StopAsync(CancellationToken cancellationToken)
method Handle (line 43) | public async Task Handle(ConfigurationChanged notification, Cancellati...
method RestartService (line 48) | private async Task RestartService(bool forceStop = false)
method UpdateConfiguration (line 57) | private async Task UpdateConfiguration()
method UpdateHostKeyFiles (line 116) | private async Task UpdateHostKeyFiles()
method StartOpenSSH (line 169) | private async Task StartOpenSSH()
method OnSSHOutput (line 211) | private void OnSSHOutput(object sender, DataReceivedEventArgs e)
method StopOpenSSH (line 219) | private async Task StopOpenSSH(bool force = false)
FILE: src/ES.SFTP/SSH/SessionHandler.cs
class SessionHandler (line 12) | public class SessionHandler : IRequestHandler<PamEventRequest, bool>
method SessionHandler (line 21) | public SessionHandler(ILogger<SessionHandler> logger, IMediator mediator)
method Handle (line 28) | [SuppressMessage("ReSharper", "MethodSupportsCancellation")]
method PrepareUserForSftp (line 46) | private async Task PrepareUserForSftp(string username)
FILE: src/ES.SFTP/Security/AuthenticationService.cs
class AuthenticationService (line 6) | public class AuthenticationService : IHostedService
method AuthenticationService (line 12) | public AuthenticationService(ILogger<AuthenticationService> logger)
method StartAsync (line 16) | public async Task StartAsync(CancellationToken cancellationToken)
method StopAsync (line 61) | public async Task StopAsync(CancellationToken cancellationToken)
FILE: src/ES.SFTP/Security/GroupUtil.cs
class GroupUtil (line 5) | public class GroupUtil
method GroupExists (line 7) | public static async Task<bool> GroupExists(string groupNameOrId)
method GroupCreate (line 13) | public static async Task GroupCreate(string name, bool force = false, ...
method GroupAddUser (line 20) | public static async Task GroupAddUser(string group, string username)
method GroupRemoveUser (line 26) | public static async Task GroupRemoveUser(string group, string username)
method GroupListUsers (line 31) | public static async Task<IReadOnlyList<string>> GroupListUsers(string ...
method GroupGetId (line 40) | public static async Task<int> GroupGetId(string groupNameOrId)
method GroupSetId (line 47) | public static async Task GroupSetId(string groupNameOrId, int id)
FILE: src/ES.SFTP/Security/UserManagementService.cs
class UserManagementService (line 10) | public class UserManagementService : IHostedService, INotificationHandle...
method UserManagementService (line 17) | public UserManagementService(ILogger<UserManagementService> logger, IM...
method StartAsync (line 24) | [SuppressMessage("ReSharper", "MethodSupportsCancellation")]
method StopAsync (line 45) | [SuppressMessage("ReSharper", "MethodSupportsCancellation")]
method Handle (line 53) | public async Task Handle(ConfigurationChanged notification, Cancellati...
method SyncUsersAndGroups (line 58) | private async Task SyncUsersAndGroups()
FILE: src/ES.SFTP/Security/UserUtil.cs
class UserUtil (line 5) | public class UserUtil
method UserExists (line 7) | public static async Task<bool> UserExists(string username)
method UserCreate (line 13) | public static async Task UserCreate(string username, bool noLoginShell...
method UserDelete (line 19) | public static async Task UserDelete(string username, bool throwOnError...
method UserSetId (line 24) | public static async Task UserSetId(string username, int id, bool nonUn...
method UserSetPassword (line 31) | public static async Task UserSetPassword(string username, string passw...
method UserGetId (line 40) | public static async Task<int> UserGetId(string username)
Condensed preview — 69 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (113K chars).
[
{
"path": ".dockerignore",
"chars": 340,
"preview": "**/.classpath\r\n**/.dockerignore\r\n**/.env\r\n**/.git\r\n**/.gitignore\r\n**/.project\r\n**/.settings\r\n**/.toolstarget\r\n**/.vs\r\n**"
},
{
"path": ".github/dependabot.yml",
"chars": 127,
"preview": "version: 2\nupdates:\n- package-ecosystem: nuget\n directory: \"/\"\n schedule:\n interval: daily\n open-pull-requests-lim"
},
{
"path": ".github/stale.yml",
"chars": 1956,
"preview": "# Configuration for probot-stale - https://github.com/probot/stale\n\n# Number of days of inactivity before an Issue or Pu"
},
{
"path": ".github/workflows/pipeline.yaml",
"chars": 7026,
"preview": "name: Pipeline\r\n\r\non:\r\n push:\r\n paths:\r\n - \"src/**\"\r\n - \".github/workflows/**\"\r\n pull_request:\r\n paths"
},
{
"path": ".gitignore",
"chars": 5584,
"preview": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## G"
},
{
"path": "LICENSE",
"chars": 1067,
"preview": "MIT License\n\nCopyright (c) 2019 emberstack\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
},
{
"path": "README.md",
"chars": 10978,
"preview": "# SFTP ([SSH File Transfer Protocol](https://en.wikipedia.org/wiki/SSH_File_Transfer_Protocol)) server using [OpenSSH](h"
},
{
"path": "samples/.ssh/id_demo2_ed25519",
"chars": 387,
"preview": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACC"
},
{
"path": "samples/.ssh/id_demo2_ed25519.pub",
"chars": 82,
"preview": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIi2wrsJnGEjfyH52+5A5JtRmkhXziftACa3tdc41vEt \n"
},
{
"path": "samples/.ssh/id_demo2_rsa",
"chars": 2610,
"preview": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn\nNhAAAAAwEAAQA"
},
{
"path": "samples/.ssh/id_demo2_rsa.pub",
"chars": 572,
"preview": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC0k+rw4Z4C7VRsjLNbsywtkrRB3JDE0AOSn9U3AqF6fbId6SK1ft6G+XrJUqJCJSB7Z6kzIfwk+qr6/gQ4"
},
{
"path": "samples/.ssh/id_demo_rsa",
"chars": 2602,
"preview": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn\nNhAAAAAwEAAQA"
},
{
"path": "samples/.ssh/id_demo_rsa.pub",
"chars": 572,
"preview": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC++8/LkNAu1DPEjnBhzjTF3dkFY+jbRDIsQ/2JkGpRdEHmcCMBTMZGL9PCEEGjWo1Lfocfnk5hWrcloTMC"
},
{
"path": "samples/hooks/onsessionchange",
"chars": 47,
"preview": "#!/bin/bash\n\necho \"Session event '$1' for '$2'\""
},
{
"path": "samples/hooks/onstartup",
"chars": 55,
"preview": "#!/bin/bash\n\necho \"SSH service startup hook completed.\""
},
{
"path": "samples/sample.dev.sftp.json",
"chars": 1429,
"preview": "{\n \"Global\": {\n \"Chroot\": {\n \"Directory\": \"%h\",\n \"StartPath\": \"sftp\"\n },\n "
},
{
"path": "samples/sample.sftp.json",
"chars": 256,
"preview": "{\n \"Global\": {\n \"Chroot\": {\n \"Directory\": \"%h\",\n \"StartPath\": \"sftp\"\n },\n "
},
{
"path": "src/.dockerignore",
"chars": 340,
"preview": "**/.classpath\r\n**/.dockerignore\r\n**/.env\r\n**/.git\r\n**/.gitignore\r\n**/.project\r\n**/.settings\r\n**/.toolstarget\r\n**/.vs\r\n**"
},
{
"path": "src/ES.SFTP/Api/PamEventsController.cs",
"chars": 978,
"preview": "using ES.SFTP.Messages.Pam;\r\nusing MediatR;\r\nusing Microsoft.AspNetCore.Mvc;\r\n\r\nnamespace ES.SFTP.Api;\r\n\r\n[Route(\"api/e"
},
{
"path": "src/ES.SFTP/Configuration/ConfigurationService.cs",
"chars": 4156,
"preview": "using ES.SFTP.Configuration.Elements;\r\nusing ES.SFTP.Messages.Configuration;\r\nusing ES.SFTP.Messages.Events;\r\nusing Med"
},
{
"path": "src/ES.SFTP/Configuration/Elements/ChrootDefinition.cs",
"chars": 175,
"preview": "namespace ES.SFTP.Configuration.Elements;\r\n\r\npublic class ChrootDefinition\r\n{\r\n public string Directory { get; set; "
},
{
"path": "src/ES.SFTP/Configuration/Elements/GlobalConfiguration.cs",
"chars": 611,
"preview": "namespace ES.SFTP.Configuration.Elements;\r\n\r\npublic class GlobalConfiguration\r\n{\r\n public ChrootDefinition Chroot { "
},
{
"path": "src/ES.SFTP/Configuration/Elements/GroupDefinition.cs",
"chars": 207,
"preview": "namespace ES.SFTP.Configuration.Elements;\r\n\r\npublic class GroupDefinition\r\n{\r\n public string Name { get; set; }\r\n "
},
{
"path": "src/ES.SFTP/Configuration/Elements/HooksDefinition.cs",
"chars": 208,
"preview": "namespace ES.SFTP.Configuration.Elements;\r\n\r\npublic class HooksDefinition\r\n{\r\n public List<string> OnServerStartup {"
},
{
"path": "src/ES.SFTP/Configuration/Elements/HostKeysDefinition.cs",
"chars": 161,
"preview": "namespace ES.SFTP.Configuration.Elements;\r\n\r\npublic class HostKeysDefinition\r\n{\r\n public string Ed25519 { get; set; "
},
{
"path": "src/ES.SFTP/Configuration/Elements/LoggingDefinition.cs",
"chars": 142,
"preview": "namespace ES.SFTP.Configuration.Elements;\r\n\r\npublic class LoggingDefinition\r\n{\r\n public bool IgnoreNoIdentificationS"
},
{
"path": "src/ES.SFTP/Configuration/Elements/SftpConfiguration.cs",
"chars": 270,
"preview": "namespace ES.SFTP.Configuration.Elements;\r\n\r\npublic class SftpConfiguration\r\n{\r\n public GlobalConfiguration Global {"
},
{
"path": "src/ES.SFTP/Configuration/Elements/UserDefinition.cs",
"chars": 627,
"preview": "namespace ES.SFTP.Configuration.Elements;\r\n\r\npublic class UserDefinition\r\n{\r\n public string Username { get; set; }\r\n"
},
{
"path": "src/ES.SFTP/Dockerfile",
"chars": 1213,
"preview": "#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for fast"
},
{
"path": "src/ES.SFTP/ES.SFTP.csproj",
"chars": 2167,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\r\n\r\n\t<PropertyGroup>\r\n\t\t<TargetFramework>net6.0</TargetFramework>\r\n\t\t<Nullable>ena"
},
{
"path": "src/ES.SFTP/Extensions/DirectoryInfoExtensions.cs",
"chars": 413,
"preview": "namespace ES.SFTP.Extensions;\r\n\r\npublic static class DirectoryInfoExtensions\r\n{\r\n public static bool IsDescendentOf("
},
{
"path": "src/ES.SFTP/Interop/ProcessRunOutput.cs",
"chars": 145,
"preview": "namespace ES.SFTP.Interop;\r\n\r\npublic class ProcessRunOutput\r\n{\r\n public string Output { get; set; }\r\n public int "
},
{
"path": "src/ES.SFTP/Interop/ProcessUtil.cs",
"chars": 1702,
"preview": "using System.Diagnostics;\r\nusing System.Text;\r\n\r\nnamespace ES.SFTP.Interop;\r\n\r\npublic class ProcessUtil\r\n{\r\n public "
},
{
"path": "src/ES.SFTP/Messages/Configuration/SftpConfigurationRequest.cs",
"chars": 176,
"preview": "using ES.SFTP.Configuration.Elements;\r\nusing MediatR;\r\n\r\nnamespace ES.SFTP.Messages.Configuration;\r\n\r\npublic class Sftp"
},
{
"path": "src/ES.SFTP/Messages/Events/ConfigurationChanged.cs",
"chars": 112,
"preview": "using MediatR;\r\n\r\nnamespace ES.SFTP.Messages.Events;\r\n\r\npublic class ConfigurationChanged : INotification\r\n{\r\n}"
},
{
"path": "src/ES.SFTP/Messages/Events/ServerStartupEvent.cs",
"chars": 110,
"preview": "using MediatR;\r\n\r\nnamespace ES.SFTP.Messages.Events;\r\n\r\npublic class ServerStartupEvent : INotification\r\n{\r\n}"
},
{
"path": "src/ES.SFTP/Messages/Events/UserSessionStartedEvent.cs",
"chars": 203,
"preview": "using MediatR;\r\n\r\nnamespace ES.SFTP.Messages.Events;\r\n\r\npublic class UserSessionChangedEvent : INotification\r\n{\r\n pu"
},
{
"path": "src/ES.SFTP/Messages/Pam/PamEventRequest.cs",
"chars": 231,
"preview": "using MediatR;\r\n\r\nnamespace ES.SFTP.Messages.Pam;\r\n\r\npublic class PamEventRequest : IRequest<bool>\r\n{\r\n public strin"
},
{
"path": "src/ES.SFTP/Program.cs",
"chars": 3422,
"preview": "using System.Reflection;\r\nusing Autofac;\r\nusing Autofac.Extensions.DependencyInjection;\r\nusing ES.SFTP.Configuration;\r\nu"
},
{
"path": "src/ES.SFTP/Properties/launchSettings.json",
"chars": 604,
"preview": "{\r\n \"$schema\": \"http://json.schemastore.org/launchsettings.json\",\r\n \"profiles\": {\r\n \"HOST\": {\r\n \"commandName\":"
},
{
"path": "src/ES.SFTP/SSH/Configuration/MatchBlock.cs",
"chars": 1466,
"preview": "using System.Text;\r\n\r\nnamespace ES.SFTP.SSH.Configuration;\r\n\r\npublic class MatchBlock\r\n{\r\n public enum MatchCriteria"
},
{
"path": "src/ES.SFTP/SSH/Configuration/SSHConfiguration.cs",
"chars": 2359,
"preview": "using System.Text;\r\n\r\nnamespace ES.SFTP.SSH.Configuration;\r\n\r\npublic class SSHConfiguration\r\n{\r\n public List<MatchBl"
},
{
"path": "src/ES.SFTP/SSH/HookRunner.cs",
"chars": 2595,
"preview": "using System.Diagnostics.CodeAnalysis;\r\nusing ES.SFTP.Interop;\r\nusing ES.SFTP.Messages.Configuration;\r\nusing ES.SFTP.Mes"
},
{
"path": "src/ES.SFTP/SSH/SSHService.cs",
"chars": 8946,
"preview": "using System.Diagnostics;\r\nusing ES.SFTP.Configuration.Elements;\r\nusing ES.SFTP.Interop;\r\nusing ES.SFTP.Messages.Config"
},
{
"path": "src/ES.SFTP/SSH/SessionHandler.cs",
"chars": 4496,
"preview": "using System.Diagnostics.CodeAnalysis;\r\nusing ES.SFTP.Configuration.Elements;\r\nusing ES.SFTP.Extensions;\r\nusing ES.SFTP."
},
{
"path": "src/ES.SFTP/Security/AuthenticationService.cs",
"chars": 3106,
"preview": "using System.Text;\r\nusing ES.SFTP.Interop;\r\n\r\nnamespace ES.SFTP.Security;\r\n\r\npublic class AuthenticationService : IHost"
},
{
"path": "src/ES.SFTP/Security/GroupUtil.cs",
"chars": 2061,
"preview": "using ES.SFTP.Interop;\r\n\r\nnamespace ES.SFTP.Security;\r\n\r\npublic class GroupUtil\r\n{\r\n public static async Task<bool> "
},
{
"path": "src/ES.SFTP/Security/UserManagementService.cs",
"chars": 7798,
"preview": "using System.Diagnostics.CodeAnalysis;\r\nusing System.Text;\r\nusing ES.SFTP.Interop;\r\nusing ES.SFTP.Messages.Configuratio"
},
{
"path": "src/ES.SFTP/Security/UserUtil.cs",
"chars": 1780,
"preview": "using ES.SFTP.Interop;\r\n\r\nnamespace ES.SFTP.Security;\r\n\r\npublic class UserUtil\r\n{\r\n public static async Task<bool> U"
},
{
"path": "src/ES.SFTP/app.logging.Development.json",
"chars": 144,
"preview": "{\r\n \"Logging\": {\r\n \"LogLevel\": {\r\n \"Default\": \"Debug\",\r\n \"System\": \"Information\",\r\n \"Microsoft\": \"Inf"
},
{
"path": "src/ES.SFTP/app.logging.json",
"chars": 573,
"preview": "{\r\n \"Serilog\": {\r\n \"Using\": [\"Serilog.Sinks.Console\"],\r\n \"MinimumLevel\": {\r\n \"Default\": \"Verbose\",\r\n \"O"
},
{
"path": "src/ES.SFTP/appsettings.Development.json",
"chars": 4,
"preview": "{\r\n}"
},
{
"path": "src/ES.SFTP/appsettings.json",
"chars": 27,
"preview": "{\r\n \"AllowedHosts\": \"*\"\r\n}"
},
{
"path": "src/ES.SFTP/config/sftp.json",
"chars": 479,
"preview": "{\r\n \"Global\": {\r\n \"Chroot\": {\r\n \"Directory\": \"%h\",\r\n \"StartPath\": \"sftp\"\r\n },\r\n \"Directories\": [\"sft"
},
{
"path": "src/ES.SFTP/config/sssd.conf",
"chars": 143,
"preview": "[sssd]\nconfig_file_version = 2\nservices = nss, pam\ndebug_level = 5\n\n[pam]\n\n[nss]\nfallback_homedir = /home/%u\ndefault_she"
},
{
"path": "src/ES.SFTP.sln",
"chars": 1123,
"preview": "\r\nMicrosoft Visual Studio Solution File, Format Version 12.00\r\n# Visual Studio Version 17\r\nVisualStudioVersion = 17.0.3"
},
{
"path": "src/ES.SFTP.sln.DotSettings",
"chars": 1334,
"preview": "<wpf:ResourceDictionary xml:space=\"preserve\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" xmlns:s=\"clr-namesp"
},
{
"path": "src/docker-compose/docker-compose.override.dev.yaml",
"chars": 412,
"preview": "version: '3'\r\nservices:\r\n sftp:\r\n image: \"emberstack/sftp:dev\"\r\n build:\r\n context: ../\r\n dockerfile: ES"
},
{
"path": "src/docker-compose/docker-compose.yaml",
"chars": 167,
"preview": "version: '3'\r\nservices:\r\n sftp:\r\n image: \"emberstack/sftp\"\r\n ports:\r\n - \"22:22\"\r\n volumes:\r\n - ../sa"
},
{
"path": "src/helm/sftp/.helmignore",
"chars": 349,
"preview": "# Patterns to ignore when building packages.\n# This supports shell glob matching, relative path matching, and\n# negation"
},
{
"path": "src/helm/sftp/Chart.yaml",
"chars": 432,
"preview": "apiVersion: v2\nname: sftp\ndescription: A Helm chart to deploy SFTP\ntype: application\nversion: 0.1.0\nappVersion: 0.1.0\n\ni"
},
{
"path": "src/helm/sftp/LICENSE",
"chars": 1086,
"preview": "MIT License\r\n\r\nCopyright (c) 2019 emberstack\r\n\r\nPermission is hereby granted, free of charge, to any person obtaining a "
},
{
"path": "src/helm/sftp/templates/NOTES.txt",
"chars": 102,
"preview": "SFTP can now be used for secure file transfers. You can connect to your service using any SFTP client."
},
{
"path": "src/helm/sftp/templates/_helpers.tpl",
"chars": 1817,
"preview": "{{/* vim: set filetype=mustache: */}}\n{{/*\nExpand the name of the chart.\n*/}}\n{{- define \"sftp.name\" -}}\n{{- default .Ch"
},
{
"path": "src/helm/sftp/templates/config-secret.yaml",
"chars": 277,
"preview": "{{- if .Values.configuration }}\napiVersion: v1\nkind: Secret\nmetadata:\n name: {{ template \"sftp.fullname\" . }}\n labels:"
},
{
"path": "src/helm/sftp/templates/deployment.yaml",
"chars": 2704,
"preview": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: {{ include \"sftp.fullname\" . }}\n labels:\n {{- include \"sftp.l"
},
{
"path": "src/helm/sftp/templates/service.yaml",
"chars": 1181,
"preview": "apiVersion: v1\nkind: Service\nmetadata:\n name: {{ include \"sftp.fullname\" . }}\n labels:\n {{- include \"sftp.labels\" ."
},
{
"path": "src/helm/sftp/templates/serviceaccount.yaml",
"chars": 315,
"preview": "{{- if .Values.serviceAccount.create -}}\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n name: {{ include \"sftp.serviceA"
},
{
"path": "src/helm/sftp/values.yaml",
"chars": 1376,
"preview": "# Default values for sftp.\n# This is a YAML-formatted file.\n# Declare variables to be passed into your templates.\n\nrepli"
}
]
About this extraction
This page contains the full source code of the emberstack/docker-sftp GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 69 files (100.3 KB), approximately 28.8k tokens, and a symbol index with 79 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.