Azure Native SFTP -- District File Uploads
Audience: Kisha (HISD liaison), district IT staff, Authentica DevOps Jira: DEVOPS-156 Storage Account:
sahisdopssftpLast 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) | |
|---|---|---|
| Host | vm-eshars-sftp IP address | sahisdopssftp.blob.core.windows.net |
| Port | 22 | 22 |
| Username | District name (e.g. springisd) | sahisdopssftp.springisd |
| Password | Shared via text file | Retrieved from eSHARS SFTP Admin app |
| Folders | SpringISD/ | 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
| Setting | Value |
|---|---|
| Protocol | SFTP (not FTP or FTPS) |
| Host | sahisdopssftp.blob.core.windows.net |
| Port | 22 |
| Username | sahisdopssftp.{your-district-slug} |
| Password | Retrieved from eSHARS SFTP Admin app |
Connecting with WinSCP
- Open WinSCP and click New Session
- Set File protocol to
SFTP - Enter the Host name:
sahisdopssftp.blob.core.windows.net - Enter the Port:
22 - Enter your Username:
sahisdopssftp.{districtslug} - Enter your Password (retrieved from eSHARS SFTP Admin app)
- Click Login
- If prompted about a host key, click Accept to trust it
Connecting with FileZilla
- Open FileZilla
- Open File > Site Manager (or press ++ctrl+s++)
- Click New Site and give it a name (e.g. “HISD SFTP - Spring ISD”)
- Set Protocol to
SFTP - SSH File Transfer Protocol - Enter the Host:
sahisdopssftp.blob.core.windows.net - Set Logon Type to
Normal - Enter your Username:
sahisdopssftp.{districtslug} - Enter your Password
- Click Connect
- 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
| Problem | Solution |
|---|---|
| ”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” warning | Accept the new key (Azure rotates these periodically) |
| “Network unreachable” | Check your firewall allows outbound connections on port 22 |
| Cannot see files | Confirm 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:
- Create a new district via the SFTP Admin app
- Retrieve credentials from the app
- 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
| Method | Route | Description |
|---|---|---|
POST | /api/districts | Create a new district |
GET | /api/districts | List all districts |
GET | /api/districts/{slug} | Get district details |
GET | /api/districts/{slug}/password | Retrieve password from Key Vault |
POST | /api/districts/{slug}/reset-password | Generate new password |
GET | /api/districts/{slug}/files | List files in district container |
GET | /api/districts/import/preview | Preview which containers need SFTP users |
POST | /api/districts/import | Bulk-create SFTP users for existing containers |
Create a District
curl -X POST https://<func-app-url>/api/districts \ -H "Content-Type: application/json" \ -d '{"name": "Peterson ISD", "notifyEmail": "kisha@example.com"}'What happens:
- District name is slugified (e.g. “Peterson ISD” →
peterson) - A blob container
petersonis created - A local SFTP user
petersonis created with read/write/list/delete permissions on the container - Azure generates an SSH password for the user
- The password is stored in Key Vault (
sftp-peterson) - 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
curl -X POST https://<func-app-url>/api/districts/peterson/reset-passwordReturns new credentials. The old password is immediately invalidated.
List All Districts
curl https://<func-app-url>/api/districtsRetrieve a Password
curl https://<func-app-url>/api/districts/peterson/passwordImport 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.
# Preview what would be importedcurl 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 onlycurl -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:
- Go to the Azure Portal → Storage Accounts →
sahisdopssftp - Under Data storage, click Containers → + Container → name it (lowercase, alphanumeric + hyphens)
- Under Security + networking, click SFTP → Add local user
- Set username (lowercase slug), enable password authentication
- Add permission scope: container name, blob service, all permissions (read, write, create, delete, list)
- Set home directory to the container name
- Click Generate password and save it to Key Vault and Bitwarden
Where Passwords Live
| Location | Purpose |
|---|---|
Key Vault (kv-hisd-sftp-{env}) | Source of truth. Secret name: sftp-{slug} |
| SFTP Admin app | Retrieves passwords from Key Vault on demand |
6. Folder Naming Convention
| Container Pattern | Purpose | Example |
|---|---|---|
{districtslug} | District file uploads | springisd |
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
StorageBlobLogstable:
StorageBlobLogs| where TimeGenerated > ago(24h)| where OperationName startswith "SftpProcess"| summarize count() by CallerIpAddress, AccountName, bin(TimeGenerated, 1h)| order by TimeGenerated desc8. Key Dates & Migration Notes
| Date | Event |
|---|---|
| 2026-03-12 | CoinMiner malware detected on vm-eshars-sftp (Defender alert) |
| 2026-03-12 | Architecture decision: Azure native SFTP over SFTPGo |
| 2026-03-16 | District folders migrated from VM to blob storage |
| 2026-03-16 | Function 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:
| Component | Path |
|---|---|
| Native SFTP module | infrastructure/modules/sftp-storage/main.tf |
| Function App admin API | src/func-eshars-sftp-admin/ |
| Migration script | scripts/sftp-migration/migrate-sftp-to-blob.ps1 |
| SFTPGo fallback (not deployed) | infrastructure/hisd-sftp/main.tf |