Skip to content

Azure Native SFTP -- District File Uploads

Audience: Kisha (HISD liaison), district IT staff, Authentica DevOps Jira: DEVOPS-156 Storage Account: sahisdopssftp Last updated: 2026-03-16


1. Overview

What Changed

HISD’s SFTP service has moved from a Windows VM running CrushFTP (vm-eshars-sftp) to Azure native SFTP backed by Azure Blob Storage. This is a fully managed service — no VMs to patch, no software to maintain.

Why

The CrushFTP VM was flagged by Microsoft Defender for Cloud with a high-severity CoinMiner malware alert. The VM had not been started in 90+ days and was running end-of-life software with SSH port 22 exposed to the internet. Rather than remediate, we replaced it with a zero-maintenance Azure-managed solution.

What’s Different for District Users

Before (CrushFTP)After (Azure SFTP)
Hostvm-eshars-sftp IP addresssahisdopssftp.blob.core.windows.net
Port2222
UsernameDistrict name (e.g. springisd)sahisdopssftp.springisd
PasswordShared via text fileRetrieved from eSHARS SFTP Admin app
FoldersSpringISD/Single lowercase container per district (e.g. springisd)

2. Architecture

District SFTP Client (WinSCP / FileZilla)
▼ port 22
┌───────────────────────────────────────┐
│ sahisdopssftp.blob.core.windows.net │
│ Azure Blob Storage (HNS/SFTP) │
├───────────────────────────────────────┤
│ Local User: sahisdopssftp.springisd │
│ → Container: springisd (read/write) │
└───────────────────────────────────────┘
▼ Management
┌────────────────────────────────────┐
│ func-eshars-sftp-admin │
│ (Azure Function App) │
│ Create districts, reset passwords │
│ Store creds in Key Vault │
└────────────────────────────────────┘

Components:

  • Storage Account (sahisdopssftp) — Data Lake Gen2 with SFTP enabled. One blob container per district.
  • Local SFTP Users — Azure-managed accounts scoped to specific containers. Password-authenticated.
  • Function App (func-eshars-sftp-admin) — Admin API for creating districts, resetting passwords, listing files. Also serves as the credential retrieval interface.
  • Key Vault (kv-hisd-sftp-{env}) — Stores generated passwords (secret prefix: sftp-).
  • Diagnostics — Transaction metrics sent to Log Analytics.

3. For HISD / Districts — Connecting via SFTP

Connection Details

SettingValue
ProtocolSFTP (not FTP or FTPS)
Hostsahisdopssftp.blob.core.windows.net
Port22
Usernamesahisdopssftp.{your-district-slug}
PasswordRetrieved from eSHARS SFTP Admin app

Connecting with WinSCP

  1. Open WinSCP and click New Session
  2. Set File protocol to SFTP
  3. Enter the Host name: sahisdopssftp.blob.core.windows.net
  4. Enter the Port: 22
  5. Enter your Username: sahisdopssftp.{districtslug}
  6. Enter your Password (retrieved from eSHARS SFTP Admin app)
  7. Click Login
  8. If prompted about a host key, click Accept to trust it

Connecting with FileZilla

  1. Open FileZilla
  2. Open File > Site Manager (or press ++ctrl+s++)
  3. Click New Site and give it a name (e.g. “HISD SFTP - Spring ISD”)
  4. Set Protocol to SFTP - SSH File Transfer Protocol
  5. Enter the Host: sahisdopssftp.blob.core.windows.net
  6. Set Logon Type to Normal
  7. Enter your Username: sahisdopssftp.{districtslug}
  8. Enter your Password
  9. Click Connect
  10. If prompted about a host key, check Always trust this host and click OK

Host Key Verification

When connecting for the first time (or after Azure rotates host keys), your SFTP client will display a host key fingerprint and ask you to trust it. This is normal and expected — click Accept or Trust.

Folder Structure

After connecting, you will see your district’s files. Each district has a single lowercase container:

  • Root (/) — Your district’s upload folder (all files)

Troubleshooting

ProblemSolution
”Connection refused”Verify you’re using port 22 and protocol SFTP (not FTP)
“Authentication failed”Check username format — must be sahisdopssftp.{slug}, not just the slug
”Host key changed” warningAccept the new key (Azure rotates these periodically)
“Network unreachable”Check your firewall allows outbound connections on port 22
Cannot see filesConfirm with Kisha that your district has been provisioned

4. For HISD / Kisha — Managing Districts

How It Works Now

HISD and Kisha can manage districts directly through the eSHARS SFTP Admin app:

  1. Create a new district via the SFTP Admin app
  2. Retrieve credentials from the app
  3. Share credentials with the district user

Password resets are also self-service through the SFTP Admin app.

What’s Needed for a New District

  • District name (e.g. “Peterson ISD”)
  • Contact email (optional — for automated credential notification when available)

5. For DevOps — District Management

Admin Function App

The func-eshars-sftp-admin Function App provides a REST API for managing SFTP districts. All endpoints are under /api/.

API Reference

MethodRouteDescription
POST/api/districtsCreate a new district
GET/api/districtsList all districts
GET/api/districts/{slug}Get district details
GET/api/districts/{slug}/passwordRetrieve password from Key Vault
POST/api/districts/{slug}/reset-passwordGenerate new password
GET/api/districts/{slug}/filesList files in district container
GET/api/districts/import/previewPreview which containers need SFTP users
POST/api/districts/importBulk-create SFTP users for existing containers

Create a District

Terminal window
curl -X POST https://<func-app-url>/api/districts \
-H "Content-Type: application/json" \
-d '{"name": "Peterson ISD", "notifyEmail": "kisha@example.com"}'

What happens:

  1. District name is slugified (e.g. “Peterson ISD” → peterson)
  2. A blob container peterson is created
  3. A local SFTP user peterson is created with read/write/list/delete permissions on the container
  4. Azure generates an SSH password for the user
  5. The password is stored in Key Vault (sftp-peterson)
  6. A notification is logged (email delivery is a TODO — currently log-only)

Response:

{
"slug": "peterson",
"sftpUsername": "sahisdopssftp.peterson",
"sftpEndpoint": "sahisdopssftp.blob.core.windows.net",
"newPassword": "<generated-password>",
"message": "District 'Peterson ISD' created. Container: peterson"
}

Reset a Password

Terminal window
curl -X POST https://<func-app-url>/api/districts/peterson/reset-password

Returns new credentials. The old password is immediately invalidated.

List All Districts

Terminal window
curl https://<func-app-url>/api/districts

Retrieve a Password

Terminal window
curl https://<func-app-url>/api/districts/peterson/password

Import Migrated Containers

After the initial migration from CrushFTP, containers exist in blob storage but may not have local SFTP users. Use the import endpoints to provision users for existing containers.

Terminal window
# Preview what would be imported
curl https://<func-app-url>/api/districts/import/preview
# Import all (creates SFTP users for containers that don't have one)
curl -X POST https://<func-app-url>/api/districts/import
# Import specific containers only
curl -X POST https://<func-app-url>/api/districts/import \
-H "Content-Type: application/json" \
-d '{"containerNames": ["springisd", "kleinisd"]}'

Post-Create Checklist

After creating a new district:

  • Verify the district appears in GET /api/districts
  • Test SFTP connection with the generated credentials
  • Share credentials with Kisha (retrieved from SFTP Admin app)
  • Kisha confirms HISD has received and tested

Manual Fallback (Azure Portal)

If the Function App is unavailable:

  1. Go to the Azure Portal → Storage Accounts → sahisdopssftp
  2. Under Data storage, click Containers+ Container → name it (lowercase, alphanumeric + hyphens)
  3. Under Security + networking, click SFTPAdd local user
  4. Set username (lowercase slug), enable password authentication
  5. Add permission scope: container name, blob service, all permissions (read, write, create, delete, list)
  6. Set home directory to the container name
  7. Click Generate password and save it to Key Vault and Bitwarden

Where Passwords Live

LocationPurpose
Key Vault (kv-hisd-sftp-{env})Source of truth. Secret name: sftp-{slug}
SFTP Admin appRetrieves passwords from Key Vault on demand

6. Folder Naming Convention

Container PatternPurposeExample
{districtslug}District file uploadsspringisd

Each district gets a single lowercase container. The slug is derived from the district name (lowercase, alphanumeric only).


7. Monitoring & Diagnostics

  • Transaction metrics are sent to Log Analytics via diagnostic settings on the storage account
  • Blob soft-delete is enabled with a 7-day retention window
  • Container soft-delete is enabled with a 7-day retention window
  • Monitor SFTP access patterns in Log Analytics using the StorageBlobLogs table:
StorageBlobLogs
| where TimeGenerated > ago(24h)
| where OperationName startswith "SftpProcess"
| summarize count() by CallerIpAddress, AccountName, bin(TimeGenerated, 1h)
| order by TimeGenerated desc

8. Key Dates & Migration Notes

DateEvent
2026-03-12CoinMiner malware detected on vm-eshars-sftp (Defender alert)
2026-03-12Architecture decision: Azure native SFTP over SFTPGo
2026-03-16District folders migrated from VM to blob storage
2026-03-16Function App deployed and active

Migration Script

The one-time migration from vm-eshars-sftp to blob storage was performed using scripts/sftp-migration/migrate-sftp-to-blob.ps1 in the eshars-ops repo. Each folder from F:\HISD-SFTP became a blob container. A marker file ({name}_{last-used-date}.txt) was placed in each container to record when the folder was last active.

Frontline File Drops


9. Infrastructure as Code

The SFTP infrastructure is defined in the eshars-ops repository:

ComponentPath
Native SFTP moduleinfrastructure/modules/sftp-storage/main.tf
Function App admin APIsrc/func-eshars-sftp-admin/
Migration scriptscripts/sftp-migration/migrate-sftp-to-blob.ps1
SFTPGo fallback (not deployed)infrastructure/hisd-sftp/main.tf