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 [![Pipeline](https://github.com/emberstack/docker-sftp/actions/workflows/pipeline.yaml/badge.svg)](https://github.com/emberstack/docker-sftp/actions/workflows/pipeline.yaml) [![Release](https://img.shields.io/github/release/emberstack/docker-sftp.svg?style=flat-square)](https://github.com/emberstack/docker-sftp/releases/latest) [![Docker Image](https://img.shields.io/docker/image-size/emberstack/sftp?style=flat-square)](https://hub.docker.com/r/emberstack/sftp) [![Docker Pulls](https://img.shields.io/docker/pulls/emberstack/sftp?style=flat-square)](https://hub.docker.com/r/emberstack/sftp) [![license](https://img.shields.io/github/license/emberstack/docker-sftp.svg?style=flat-square)](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 _logger; private readonly IMediator _mediator; public PamEventsController(ILogger logger, IMediator mediator) { _logger = logger; _mediator = mediator; } [HttpGet] [Route("generic")] public async Task 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 { private readonly ILogger _logger; private readonly IMediator _mediator; private readonly IOptionsMonitor _sftpOptionsMonitor; private SftpConfiguration _config; private IDisposable _sftpOptionsMonitorChangeHandler; public ConfigurationService(ILogger logger, IOptionsMonitor 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 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(); 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(); var validUsers = new List(); 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(); 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 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 Users { get; set; } = new(); } ================================================ FILE: src/ES.SFTP/Configuration/Elements/HooksDefinition.cs ================================================ namespace ES.SFTP.Configuration.Elements; public class HooksDefinition { public List OnServerStartup { get; set; } = new(); public List 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 Users { get; set; } = new(); public List 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 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 Directories { get; set; } = new(); public List 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 ================================================  net6.0 enable enable Linux emberstack/sftp:dev -p 2222:22 -p 25080:25080 --name sftpdev --privileged false <_ContentIncludedByDefault Remove="app.logging.Development.json" /> <_ContentIncludedByDefault Remove="app.logging.json" /> <_ContentIncludedByDefault Remove="config\sftp.json" /> PreserveNewest PreserveNewest true PreserveNewest PreserveNewest true PreserveNewest ================================================ 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 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 { } ================================================ 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 { 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(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(ctx => { var c = ctx.Resolve(); return t => c.Resolve(t); }); container.RegisterType().AsImplementedInterfaces().SingleInstance(); container.RegisterType().AsImplementedInterfaces().SingleInstance(); container.RegisterType().AsImplementedInterfaces().SingleInstance(); container.RegisterType().AsImplementedInterfaces().SingleInstance(); container.RegisterType().AsImplementedInterfaces().SingleInstance(); container.RegisterType().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 Match { get; set; } = new(); public List Except { get; set; } = new(); public List Declarations { get; set; } = new(); private string GetPatternLine() { var builder = new StringBuilder(); builder.Append($"Match {Criteria} "); var patternList = (Match ?? new List()).Where(s => !string.IsNullOrWhiteSpace(s)) .Select(s => $"{s.Trim()}").Distinct().ToList(); patternList.AddRange((Except ?? new List()).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()).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 MatchBlocks { get; } = new(); public List 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, INotificationHandler { private readonly ILogger _logger; private readonly IMediator _mediator; public HookRunner(ILogger 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(); 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(); 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 { 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 _logger; private readonly IMediator _mediator; private bool _loggingIgnoreNoIdentificationString; private Process _serverProcess; private Action _serviceProcessExitAction; public SSHService(ILogger 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(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(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 { 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 logger, IMediator mediator) { _logger = logger; _mediator = mediator; } [SuppressMessage("ReSharper", "MethodSupportsCancellation")] public async Task 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(); 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 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 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> 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 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 { private const string HomeBasePath = "/home"; private const string SftpUserInventoryGroup = "sftp-user-inventory"; private readonly ILogger _logger; private readonly IMediator _mediator; public UserManagementService(ILogger 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(); 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 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 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 ================================================  SFTP SSH SSHD SSSD True True True True True True ================================================ 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: {}