Stdout logging for .NET core while WEBSITE_RUN_FROM_PACKAGE is set to true

When Azure DevOps deploys .NET core application to Azure App Service it automatically sets value of WEBSITE_RUN_FROM_PACKAGE to 1. This setting is causing website root directory become readonly and stdout logging from .NET core process would fail within that directory. So if you web.config looks like below it will stop working as a result.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <system.webServer>
        <modules>
            <remove name="WebDAVModule" />
        </modules>
        <handlers>
            <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
        </handlers>
<aspNetCore processPath="dotnet" arguments=".\binary.dll" stdoutLogEnabled="true" stdoutLogFile=".\log" forwardWindowsAuthToken="false"/>
    </system.webServer>
</configuration>

With WEBSITE_RUN_FROM_PACKAGE even though wwwroot is locked, home directory is not, which is d:\home\LogFiles, so web.config shall be transformed during deployment, so value of <aspNetCore processPath="dotnet" arguments=".\binary.dll" stdoutLogEnabled="true" stdoutLogFile=".\log" forwardWindowsAuthToken="false"/> shall be transformed into <aspNetCore processPath="dotnet" arguments=".\binary.dll" stdoutLogEnabled="true" stdoutLogFile="d:\home\LogFiles" forwardWindowsAuthToken="false"/>

For this to happen web.config transformation shall be applied during release pipeline. Transformation happens based on file which will be named based on environment where this setting shall be applied to. For example if you have environment which is called qa-blue then transformation file shall be called web.qa-blue.config and have following content which instructs transform engine to replace value of aspNetCore node.

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
	<system.webServer>
		<modules>
			<remove name="WebDAVModule" />
		</modules>
		<handlers>
			<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified"/>
		</handlers>
		<aspNetCore processPath="dotnet" arguments=".\binary.dll" stdoutLogEnabled="true" stdoutLogFile="d:\home\LogFiles" forwardWindowsAuthToken="false" xdt:Transform="Replace" />
	</system.webServer>
</configuration>

Release pipeline would need to indicate that XML transformation happen during AppService deployment to QA-blue environment

You would be able to see in output transformation taking place

022-09-14T15:54:41.7194241Z Start tranformation to 'D:\a\_temp\temp_web_package_9382618325923051\web.config'.
2022-09-14T15:54:41.7195325Z Source file: 'D:\a\_temp\temp_web_package_9382618325923051\web.config'.
2022-09-14T15:54:41.7196147Z Transform  file: 'D:\a\_temp\temp_web_package_9382618325923051\web.QA-blue.config'.
2022-09-14T15:54:41.7197018Z Transformation task is using encoding 'System.Text.UTF8Encoding'. Change encoding in source file, or use the 'encoding' parameter if you want to change encoding.
2022-09-14T15:54:41.7197772Z Executing Replace (transform line 16, 153)
2022-09-14T15:54:41.7198321Z on /configuration/system.webServer/aspNetCore
2022-09-14T15:54:41.7198896Z Applying to 'aspNetCore' element (no source line info)
2022-09-14T15:54:41.7199412Z Replaced 'aspNetCore' element
2022-09-14T15:54:41.7199887Z Done executing Replace
2022-09-14T15:54:41.7200481Z XML Transformations applied successfully
Advertisement

How to use Azure ACI (Azure container instances) to mine crypto

Steps below will allow you to mine crypto currency via Azure Container Instances with GPU capability. This is just for research purposes since it will cost more to run those compared to return.

ACI is chosen since it’s super simple to setup and maintain compared to any other possible choices of hosting GPU compute in Azure.

All code is available here

I choose TREX as mining rig and my DOCKERFILE to is below.

FROM nvidia/cuda:11.4.2-base-ubuntu20.04
COPY trex .
RUN chmod -x ETH*
EXPOSE 80
CMD /bin/sh

You need to modify ETH-2miners.sh with your wallet ID. I specifically use 2miners.com pool and mining nanocoin so my requirements look similar to below

#!/bin/sh
./t-rex -a ethash -o stratum+tcp://eth.2miners.com:2020 -u nano_1gfxg1aqgrph7o3harn3tn88687nxhsnxiwj4p3gch6sdgycchwrjpmwhrmq -p x -w rig0 --api-bind-http 0.0.0.0:80 --api-read-only --api-key bwAAAAAAAABLgYyRCwNorPL/PYCbJzeLdfDyIpJV0sfmzMJxT05q0E2N5Dz9glrBtL4Kt9l/KmaDlzrZ0hW7RKE63Y1b3B1WCD4TIkhqFB8=

You can see example parameters files here

How to performing automated Selenium UI tests in Azure DevOps release pipelines

Instructions below will allow you to perform automated UI (functional) tests using Selenium in Azure DevOps release pipeline. Results of tests are automatically published to release pipeline and can be used as gates to propagate to next environment(stage) in pipeline.

Functional tests task consists of following main tasks:

  1. Download specific version of Selenium chromedriver and Google chrome
  2. Downloading and ensuring correct version of .NET core is used
  3. Install chrome driver and chrome browser
  4. Install Visual Studio Test platform
  5. Perform Visual Studio tests

This is how release pipeline task will look like

JSON based export is below which will allow you to import it into your pipeline

{
   "tasks": [
      {
         "environment": {},
         "displayName": "Download chromedriver version $(chromedriverversion)",
         "alwaysRun": false,
         "continueOnError": false,
         "condition": "succeeded()",
         "enabled": true,
         "timeoutInMinutes": 0,
         "retryCountOnTaskFailure": 0,
         "inputs": {
            "targetType": "inline",
            "filePath": "",
            "arguments": "",
            "script": "$ProgressPreference = 'SilentlyContinue'\nInvoke-webrequest \"http://chromedriver.storage.googleapis.com/$(chromedriverversion)/chromedriver_win32.zip\" -outfile driver.zip -usebasicparsing\ninvoke-webrequest \"https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Win%2F982528%2Fchrome-win.zip?generation=1647568388173534&alt=media\" -outfile chrome.zip -usebasicparsing\n",
            "errorActionPreference": "stop",
            "warningPreference": "default",
            "informationPreference": "default",
            "verbosePreference": "continue",
            "debugPreference": "default",
            "failOnStderr": "false",
            "showWarnings": "true",
            "ignoreLASTEXITCODE": "false",
            "pwsh": "false",
            "workingDirectory": "",
            "runScriptInSeparateScope": "false"
         },
         "task": {
            "id": "e213ff0f-5d5c-4791-802d-52ea3e7be1f1",
            "versionSpec": "2.*",
            "definitionType": "task"
         }
      },
      {
         "environment": {},
         "displayName": "Use .NET Core sdk 6.x",
         "alwaysRun": false,
         "continueOnError": false,
         "condition": "succeeded()",
         "enabled": true,
         "timeoutInMinutes": 0,
         "retryCountOnTaskFailure": 0,
         "inputs": {
            "packageType": "sdk",
            "useGlobalJson": "false",
            "workingDirectory": "",
            "version": "6.x",
            "vsVersion": "",
            "includePreviewVersions": "false",
            "installationPath": "$(Agent.ToolsDirectory)/dotnet",
            "performMultiLevelLookup": "false"
         },
         "task": {
            "id": "b0ce7256-7898-45d3-9cb5-176b752bfea6",
            "versionSpec": "2.*",
            "definitionType": "task"
         }
      },
      {
         "environment": {},
         "displayName": "Install Google Chrome",
         "alwaysRun": false,
         "continueOnError": false,
         "condition": "succeeded()",
         "enabled": true,
         "timeoutInMinutes": 0,
         "retryCountOnTaskFailure": 0,
         "inputs": {
            "targetType": "inline",
            "filePath": "",
            "arguments": "",
            "script": "expand-archive chrome.zip -DestinationPath .   \nMove-Item .\\chrome-win\\* $(System.DefaultWorkingDirectory)/code/tests\n",
            "errorActionPreference": "stop",
            "warningPreference": "default",
            "informationPreference": "default",
            "verbosePreference": "default",
            "debugPreference": "default",
            "failOnStderr": "false",
            "showWarnings": "false",
            "ignoreLASTEXITCODE": "false",
            "pwsh": "false",
            "workingDirectory": "",
            "runScriptInSeparateScope": "false"
         },
         "task": {
            "id": "e213ff0f-5d5c-4791-802d-52ea3e7be1f1",
            "versionSpec": "2.*",
            "definitionType": "task"
         }
      },
      {
         "environment": {},
         "displayName": "Extract chromedriver",
         "alwaysRun": false,
         "continueOnError": false,
         "condition": "succeeded()",
         "enabled": true,
         "timeoutInMinutes": 0,
         "retryCountOnTaskFailure": 0,
         "inputs": {
            "archiveFilePatterns": "**/driver.zip",
            "destinationFolder": "$(System.DefaultWorkingDirectory)/code/tests",
            "cleanDestinationFolder": "false",
            "overwriteExistingFiles": "false",
            "pathToSevenZipTool": ""
         },
         "task": {
            "id": "5e1e3830-fbfb-11e5-aab1-090c92bc4988",
            "versionSpec": "1.*",
            "definitionType": "task"
         }
      },
      {
         "environment": {},
         "displayName": "Visual Studio Test Platform Installer",
         "alwaysRun": false,
         "continueOnError": false,
         "condition": "succeeded()",
         "enabled": true,
         "timeoutInMinutes": 0,
         "retryCountOnTaskFailure": 0,
         "inputs": {
            "packageFeedSelector": "nugetOrg",
            "versionSelector": "latestPreRelease",
            "testPlatformVersion": "",
            "customFeed": "",
            "username": "",
            "password": "",
            "netShare": ""
         },
         "task": {
            "id": "2c65196a-54fd-4a02-9be8-d9d1837b7111",
            "versionSpec": "1.*",
            "definitionType": "task"
         }
      },
      {
         "environment": {},
         "displayName": "VsTest - testAssemblies",
         "alwaysRun": false,
         "continueOnError": false,
         "condition": "succeeded()",
         "enabled": true,
         "timeoutInMinutes": 0,
         "retryCountOnTaskFailure": 0,
         "inputs": {
            "testSelector": "testAssemblies",
            "testAssemblyVer2": "**/FunctionalTests.dll",
            "testPlan": "",
            "testSuite": "",
            "testConfiguration": "",
            "tcmTestRun": "$(test.RunId)",
            "searchFolder": "$(System.DefaultWorkingDirectory)",
            "resultsFolder": "$(Agent.TempDirectory)\\TestResults",
            "testFiltercriteria": "",
            "runOnlyImpactedTests": "False",
            "runAllTestsAfterXBuilds": "50",
            "uiTests": "true",
            "vstestLocationMethod": "version",
            "vsTestVersion": "toolsInstaller",
            "vstestLocation": "",
            "runSettingsFile": "",
            "overrideTestrunParameters": "",
            "pathtoCustomTestAdapters": "",
            "runInParallel": "false",
            "runTestsInIsolation": "False",
            "codeCoverageEnabled": "False",
            "otherConsoleOptions": "",
            "distributionBatchType": "basedOnTestCases",
            "batchingBasedOnAgentsOption": "autoBatchSize",
            "customBatchSizeValue": "10",
            "batchingBasedOnExecutionTimeOption": "autoBatchSize",
            "customRunTimePerBatchValue": "60",
            "dontDistribute": "False",
            "testRunTitle": "",
            "platform": "",
            "configuration": "",
            "publishRunAttachments": "true",
            "failOnMinTestsNotRun": "False",
            "minimumExpectedTests": "1",
            "diagnosticsEnabled": "false",
            "collectDumpOn": "onAbortOnly",
            "rerunFailedTests": "false",
            "rerunType": "basedOnTestFailurePercentage",
            "rerunFailedThreshold": "30",
            "rerunFailedTestCasesMaxLimit": "5",
            "rerunMaxAttempts": "3"
         },
         "task": {
            "id": "ef087383-ee5e-42c7-9a53-ab56c98420f9",
            "versionSpec": "2.*",
            "definitionType": "task"
         }
      }
   ],
   "runsOn": [
      "Agent"
   ],
   "revision": 48,
   "createdBy": {
      "displayName": "Gregory Suvalian",
      "id": "8ca32597-0a36-6e0b-8619-e82e68f1c27c",
      "uniqueName": "email@domain.com"
   },
   "createdOn": "2022-06-13T15:50:23.720Z",
   "modifiedBy": {
      "displayName": "Gregory Suvalian",
      "id": "8ca32597-0a36-6e0b-8619-e82e68f1c27c",
      "uniqueName": "email@domain.com"
   },
   "modifiedOn": "2022-08-16T21:04:05.143Z",
   "comment": "",
   "id": "ee997a99-9e45-42d3-8d17-df4f65dd72b4",
   "name": "Functional-Tests",
   "version": {
      "major": 1,
      "minor": 0,
      "patch": 0,
      "isTest": false
   },
   "iconUrl": "https://cdn.vsassets.io/v/M204_20220608.4/_content/icon-meta-task.png",
   "friendlyName": "Functional-Tests",
   "description": "",
   "category": "Deploy",
   "definitionType": "metaTask",
   "author": "Gregory Suvalian",
   "demands": [],
   "groups": [],
   "inputs": [
      {
         "aliases": [],
         "options": {},
         "properties": {},
         "name": "chromedriverversion",
         "label": "chromedriverversion",
         "defaultValue": "101.0.4951.41",
         "required": true,
         "type": "string",
         "helpMarkDown": "",
         "groupName": ""
      }
   ],
   "satisfies": [],
   "sourceDefinitions": [],
   "dataSourceBindings": [],
   "instanceNameFormat": "Task group: Functional-Tests $(chromedriverversion)",
   "preJobExecution": {},
   "execution": {},
   "postJobExecution": {}
}

SFTP access to Azure Files via windows nanoserver container

While azure blog storage now supports FTP service access – it’s available only in preview currently and only in certain locations. So if you have task to provide SFTP access to Azure backed storage there is not a lot of choices short of full blown IaaS implementation or container based solution (https://github.com/atmoz/sftp). If your backing is Azure files then there is no currently a clean way to have multi-user access to backend storage without creating share for each individual user.

Solution outlined below provides SFTP service access based on nano server based container (image is ~200 MB) which is comparable to Linux based solutions and does not have an issue of using single Azure fileshare for all sftp users.

Entire solution and file are available at https://github.com/artisticcheese/Azure/tree/main/sftp

DOCKERFILE with explanation is below.

  • Base image is based off powershell image for automation purposes
  • Build argument (TAG) is being used to provide facility to build for different host OS (current automatic weekly build is provided for lts-nanoserver-1809 and lts-nanoserver-ltsc2022 tags)
  • ContainerAdministrator user is used to perform system level operations necessary for OpenSSH to function
  • ENTRYPOINT performs housekeeping items such as
    • Create users based on configuration file
    • Create sftp folders for those users if they don’t exist yet
    • Copy configuration for sshd service to proper location
    • Run script to fix necessary permissions for sshd to work
    • Starts SSHD service
    • Reads and outputs to stdout SSHD logs
#escape = `
ARG TAG=lts-nanoserver-1809
FROM mcr.microsoft.com/powershell:$TAG
SHELL ["pwsh", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]
USER ContainerAdministrator
WORKDIR c:\prep
RUN Invoke-WebRequest "https://github.com/PowerShell/Win32-OpenSSH/releases/download/V8.6.0.0p1-Beta/OpenSSH-Win64.zip" -OutFile OpenSSH-Win64.zip -UseBasicParsing; `
   Expand-Archive OpenSSH-Win64.zip .; Remove-Item OpenSSH-Win64.zip`
   & .\OpenSSH-Win64\install-sshd.ps1; `
   Set-Service sshd -StartupType Manual; Set-Service ssh-agent -StartupType Manual; `
   sc.exe failure sshd reset= 86400 actions= restart/500 
ENTRYPOINT if ($env:bootstrapLocation) {Invoke-Expression (Invoke-WebRequest $env:bootstrapLocation).Content } ;`
   foreach ($user in (Get-Content $env:configLocation\users.json | convertfrom-JSON).users) { `
   net user /add $user.username $user.password ; `
   (Test-Path "$env:sftpLocation\$($user.username)") ? (Write-Output "Folder already exists"):(New-Item -ItemType Directory -path "$env:sftpLocation\$($user.username)"); `
   };`
   mkdir c:\programdata\ssh\; Copy-Item -Path $env:configLocation\* -Destination c:\programdata\ssh\ ;  & .\OpenSSH-Win64\FixHostFilePermissions.ps1 -Confirm:$false; & .\OpenSSH-Win64\FixUserFilePermissions.ps1 -Confirm:$false; `
   Start-Service sshd; `
   while ($true) {get-content C:\ProgramData\ssh\logs\sshd.log -tail 1 -wait} 

To run this docker image you need to provide location where SFTP files will be located (docker volume mount) and location where configuration files are.

Configuration files:

  • sshd_config file which:
    • Configures SSH to allow only SFTP user for users
    • Separate users into their folders
    • Configure extending logging to local file system
  • SSH host keys (ssh_host_ed25519_key, ssh_host_rsa_key etc). You can copy those files into configuration volume, otherwise those will be recreated by SSHd daemon and your users will be prompted to accept new keys on each restart
  • users.json file with username/passwords for SFTP users

2 environment variables are setting location of those 2 folders will be within running container:

  • configLocation – location of folder containing config files above within container
  • sftpLocation – location of folder where SFTP files will be located at

Example compose file so you can this locally via Docker Desktop is below

version: "3.9"
services:
  sftp:
    build: .
    image: artisticcheese/sftp:lts-nanoserver-1809
    ports:
      - "22:22"
    volumes:
      - type: bind
        source: C:\repo\Azure\sftp\config
        target: c:\config\
      - type: bind
        source: c:\repo\Azure\sftp\data\
        target: c:\sftp\
    environment:
      configLocation: "C:\\config\\"
      sftpLocation: "C:\\sftp\\"
networks:
  default: null

To deploy to AKS you can use Yaml examples below. You’d need to substitute your own hostfiles as well as storage account keys but they rest can stay the same

Secrets.yaml

apiVersion: v1
kind: Namespace
metadata:
  name: "sftp"
---
apiVersion: v1
kind: Secret
metadata:
  name: sftp-secret
  namespace: sftp
type: Opaque
stringData:
  users.json: |
    {
      "users": [
          {
            "username": "greg",
            "password": "123"
          },
          {
            "username": "socrates",
            "password": "1234"
          }
      ]
    }
  sshd_config: |
    # This is the sshd server system-wide configuration file.  See
    # sshd_config(5) for more information.

    # The strategy used for options in the default sshd_config shipped with
    # OpenSSH is to specify options with their default value where
    # possible, but leave them commented.  Uncommented options override the
    # default value.

    #Port 22
    #AddressFamily any
    #ListenAddress 0.0.0.0
    #ListenAddress ::

    #HostKey __PROGRAMDATA__/ssh/ssh_host_rsa_key
    #HostKey __PROGRAMDATA__/ssh/ssh_host_dsa_key
    #HostKey __PROGRAMDATA__/ssh/ssh_host_ecdsa_key
    #HostKey __PROGRAMDATA__/ssh/ssh_host_ed25519_key

    # Ciphers and keying
    #RekeyLimit default none

    # Logging
    SyslogFacility LOCAL0
    LogLevel DEBUG3

    # Authentication:

    #LoginGraceTime 2m
    #PermitRootLogin prohibit-password
    #StrictModes yes
    #MaxAuthTries 6
    #MaxSessions 10

    #PubkeyAuthentication yes

    # The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2
    # but this is overridden so installations will only check .ssh/authorized_keys
    AuthorizedKeysFile	.ssh/authorized_keys

    #AuthorizedPrincipalsFile none

    # For this to work you will also need host keys in %programData%/ssh/ssh_known_hosts
    #HostbasedAuthentication no
    # Change to yes if you don't trust ~/.ssh/known_hosts for
    # HostbasedAuthentication
    #IgnoreUserKnownHosts no
    # Don't read the user's ~/.rhosts and ~/.shosts files
    #IgnoreRhosts yes

    # To disable tunneled clear text passwords, change to no here!
    #PasswordAuthentication yes
    #PermitEmptyPasswords no

    # GSSAPI options
    #GSSAPIAuthentication no

    #AllowAgentForwarding yes
    #AllowTcpForwarding yes
    #GatewayPorts no
    #PermitTTY yes
    #PrintMotd yes
    #PrintLastLog yes
    #TCPKeepAlive yes
    #UseLogin no
    #PermitUserEnvironment no
    #ClientAliveInterval 0
    #ClientAliveCountMax 3
    #UseDNS no
    #PidFile /var/run/sshd.pid
    #MaxStartups 10:30:100
    #PermitTunnel no
    ChrootDirectory c:\sftp\
    #VersionAddendum none

    # no default banner path
    #Banner none

    # override default of no subsystems
    Subsystem	sftp	sftp-server.exe  -f LOCAL0 -l DEBUG3 -d "c:\sftp\"

    # Example of overriding settings on a per-user basis
    #Match User anoncvs
    #	AllowTcpForwarding no
    #	PermitTTY no
      ForceCommand internal-sftp

    Match User *
    ChrootDirectory c:\sftp\%u

    Match Group administrators
          AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys
  ssh_host_ed25519_key: |
    -----BEGIN OPENSSH PRIVATE KEY-----
    b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
    QyNTUxOQAAACBkqePL1p4dFDk3DQOxu4zlyGa+eTZssX9uCgJCve1DUgAAALC+1suHvtbL
    hwAAAAtzc2gtZWQyNTUxOQAAACBkqePL1p4dFDk3DQOxu4zlyGa+eTZssX9uCgJCve1DUg
    AAAECEVjFuAEZXqvb9AenBvAknXZn7QUmgEHDlIFOJVu73D2Sp48vWnh0UOTcNA7G7jOXI
    Zr55Nmyxf24KAkK97UNSAAAAK3N5c3RlbUA3MDMzYTgxNC1hZTg3LTRkMjAtYmU0My05ZW
    NjYjM2MTE0ZTIBAg==
    -----END OPENSSH PRIVATE KEY-----
  ssh_host_rsa_key: |
    -----BEGIN OPENSSH PRIVATE KEY-----
    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
    MTRlMg==
    -----END OPENSSH PRIVATE KEY-----

---
apiVersion: v1
data:
  azurestorageaccountkey: N3g1eUtYMFpnVCtMa2NoNHpMUGFtZZMEFYS2FsN3lSVjVoNUJrTWJWaVVHeklHb2RXcDJLSkMzblVOK3BWb3NJM0lmam9DWTBFSlppN3ZERk45Umc9PQ==
  azurestorageaccountname: Y29yZTM2MHN0zaGFyZQ==
kind: Secret
metadata:
  name: fileshare-secret
  namespace: sftp
type: Opaque

Deployment.yaml

apiVersion: v1
kind: Service
metadata:
  name: sftp-service
  namespace: sftp
  labels:
    app: sftp
spec:
  type: LoadBalancer
  selector:
    app: sftp
  ports:
    - protocol: TCP
      name: ssh
      port: 22
      targetPort: 22
---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: sftp
  name: "sftp"
  labels:
    app: "sftp"
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sftp
  template:
    metadata:
      labels:
        app: sftp
      namespace: sftp
    spec:
      containers:
        - name: sftp
          image: artisticcheese/sftp:lts-nanoserver-1809
          imagePullPolicy: Always
          env:
            - name: ConfigLocation
              value: "c:\\config"
            - name: sftpLocation
              value: "c:\\sftp"
          ports:
            - containerPort: 22
          volumeMounts:
            - name: ssh-host
              mountPath: "c:\\config\\"
              readOnly: true
            - name: sftp
              mountPath: "c:\\sftp\\"
              readOnly: false
      volumes:
        - name: sftp
          azureFile:
            secretName: fileshare-secret
            shareName: sftp
            readOnly: false
        - name: ssh-host
          secret:
            secretName: sftp-secret

Once deployed you shall be able to sftp to public IP of load balancers and perform SFTP operations

PS C:\test> sftp greg@20.127.178.46
The authenticity of host '20.127.178.46 (20.127.178.46)' can't be established.
ECDSA key fingerprint is SHA256:PL9yk8WCsA+em/KJIcPZVnfR14GiurqQYxOQMHwy6Bk.
Are you sure you want to continue connecting (yes/no/[fingerprint])?
Warning: Permanently added '20.127.178.46' (ECDSA) to the list of known hosts.
greg@20.127.178.46's password:
Connected to 20.127.178.46.
sftp> put c:\test\a.zip
Uploading c:/test/a.zip to /a.zip
c:/test/a.zip                                                                                                                                                                                                                               100% 3574KB   3.8MB/s   00:00