Skip to content

Instantly share code, notes, and snippets.

@DoumanAsh
Last active September 15, 2026 22:08
Show Gist options
  • Select an option

  • Save DoumanAsh/c8eacc4a58d1682b17dbbdd2655d7d68 to your computer and use it in GitHub Desktop.

Select an option

Save DoumanAsh/c8eacc4a58d1682b17dbbdd2655d7d68 to your computer and use it in GitHub Desktop.
Terraform module to deploy nginx-sftp pod to provide ability to test ssh/sftp alongside having easy access via HTTP listing
variable "storage_class_name" {
description = "Storage class to use to request persistent volume"
type = string
nullable = true
default = null
}
variable "ssh_public_key" {
description = "SSH public key for authentication"
type = string
}
variable "user_name" {
description = "Username for the SSH user"
type = string
default = "user"
}
variable "password" {
description = "Password for the HTTP user. SSH user doesn't have password auth enabled"
type = string
sensitive = true
}
output "sftp_service_port" {
description = "Internal Port of the SSH service"
value = local.sftp_ext_port
}
output "sftp_service_name" {
description = "Service name of the SSH service"
value = kubernetes_service_v1.openssh_server.metadata.0.name
}
output "http_service_port" {
description = "External Port of the HTTP service"
value = local.http_ext_port
}
output "http_service_name" {
description = "Service name of the HTTP service"
value = kubernetes_service_v1.http_server.metadata.0.name
}
terraform {
required_providers {
htpasswd = {
source = "loafoe/htpasswd"
version = ">= 1, < 2"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = ">= 2"
}
}
}
locals {
sftp_http_service_name = "dev-sftp-http-server"
http_service_name = "dev-file-server"
sftp_service_name = "dev-openssh-server"
sftp_ext_port = 12222
http_ext_port = 12223
}
# Persistent volume for sftp data
resource "kubernetes_persistent_volume_claim_v1" "sftp_file_data" {
metadata {
name = "sftp-server-data"
}
spec {
access_modes = ["ReadWriteOnce"]
resources {
requests = {
storage = "128Mi"
}
}
storage_class_name = var.storage_class_name
}
# Dynamic provisioning means you will mount volume when pod requests it
wait_until_bound = false
}
# Prepare httpasswd file for basic auth in nginx
resource "random_password" "salt" {
length = 8
special = false
}
resource "htpasswd_password" "password" {
password = var.password
salt = random_password.salt.result
}
resource "kubernetes_secret_v1" "nginx_htpasswd" {
metadata {
name = "nginx-htpasswd"
}
data = {
"password" = "${var.password}"
".htpasswd" = "${var.user_name}:${htpasswd_password.password.bcrypt}"
}
type = "Opaque"
}
# Prepare persistent host certificates to ensure stable fingerprint
resource "tls_private_key" "sftp_host_ecdsa_key" {
algorithm = "ECDSA"
ecdsa_curve = "P256"
}
resource "tls_private_key" "sftp_host_rsa_key" {
algorithm = "RSA"
rsa_bits = 4096
}
resource "kubernetes_secret_v1" "sftp_host_keys" {
metadata {
name = "sftp-host-keys"
}
data = {
"ssh_host_ecdsa_key" = "${tls_private_key.sftp_host_ecdsa_key.private_key_openssh}"
"ssh_host_ecdsa_key.pub" = "${tls_private_key.sftp_host_ecdsa_key.public_key_openssh}"
"ssh_host_rsa_key" = "${tls_private_key.sftp_host_rsa_key.private_key_openssh}"
"ssh_host_rsa_key.pub" = "${tls_private_key.sftp_host_rsa_key.public_key_openssh}"
}
type = "Opaque"
}
# Deploy sftp + nginx together
resource "kubernetes_deployment_v1" "sftp_http_server" {
metadata {
name = local.sftp_http_service_name
}
spec {
replicas = 1
strategy {
type = "Recreate"
}
selector {
match_labels = {
app = local.sftp_http_service_name
}
}
template {
metadata {
labels = {
app = local.sftp_http_service_name
}
}
spec {
container {
name = local.sftp_http_service_name
image = "quay.io/doumanash/nginx-sftp:3.24"
port {
container_port = local.http_ext_port
}
port {
container_port = local.sftp_ext_port
}
# Define readiness by SSH service
readiness_probe {
tcp_socket {
port = local.sftp_ext_port
}
initial_delay_seconds = 10
period_seconds = 10
}
# Ping HTTP for probing liveness just in case
liveness_probe {
http_get {
path = "/"
port = local.http_ext_port
}
initial_delay_seconds = 15
period_seconds = 30
}
# no need for heavy usage so minimize resources limits
resources {
requests = {
cpu = "50m"
memory = "64Mi"
}
limits = {
cpu = "100m"
memory = "128Mi"
}
}
# Common definitions for user account
env {
name = "USER_NAME"
value = var.user_name
}
env {
name = "SUDO_ACCESS"
value = "true"
}
env {
name = "PUID"
value = "2000"
}
env {
name = "PGID"
value = "2000"
}
env {
name = "UMASK"
value = "0022"
}
env {
name = "TZ"
value = "Etc/UTC"
}
# SSH settings
env {
name = "SSH_PORT"
value = local.sftp_ext_port
}
env {
name = "PUBLIC_KEY"
value = var.ssh_public_key
}
env {
name = "PASSWORD_ACCESS"
value = "true"
}
env {
name = "USER_PASSWORD"
value_from {
secret_key_ref {
key = "password"
name = kubernetes_secret_v1.nginx_htpasswd.metadata[0].name
}
}
}
# Define how to serve static files
env {
name = "HTTP_PORT"
value = local.http_ext_port
}
env {
name = "HTTP_SERVE_ROUTE"
value = "/sftp"
}
env {
name = "HTTP_SERVE_UPLOAD_ROUTE"
value = "/upload-sftp"
}
env {
name = "HTTP_SERVE_FOLDER"
value = "/sftp-data"
}
# Keep data only for 14 days to avoid polluting it with too much sftp uploads
env {
name = "HTTP_SERVE_FOLDER_RETENTION_DAYS"
value = "14"
}
volume_mount {
name = "shared-data"
mount_path = "/sftp-data"
}
volume_mount {
name = "htpasswd"
mount_path = "/config/nginx/.htpasswd/"
sub_path = ".htpasswd"
}
volume_mount {
name = "config-folder"
mount_path = "/config"
}
# SFTP host keys
volume_mount {
name = "ssh-host-ecdsa-key"
mount_path = "/config/ssh_host_keys/ssh_host_ecdsa_key"
sub_path = "ssh_host_ecdsa_key"
} # ssh-host-ecdsa-key
volume_mount {
name = "ssh-host-ecdsa-key-pub"
mount_path = "/config/ssh_host_keys/ssh_host_ecdsa_key.pub"
sub_path = "ssh_host_ecdsa_key.pub"
} # ssh-host-ecdsa-key-pub
volume_mount {
name = "ssh-host-rsa-key"
mount_path = "/config/ssh_host_keys/ssh_host_rsa_key"
sub_path = "ssh_host_rsa_key"
} # ssh-host-rsa-key
volume_mount {
name = "ssh-host-rsa-key-pub"
mount_path = "/config/ssh_host_keys/ssh_host_rsa_key.pub"
sub_path = "ssh_host_rsa_key.pub"
} # ssh-host-rsa-key-pub
}
security_context {
fs_group = 2000
}
volume {
name = "shared-data"
persistent_volume_claim {
claim_name = kubernetes_persistent_volume_claim_v1.sftp_file_data.metadata[0].name
}
} //shared-data
volume {
name = "htpasswd"
secret {
secret_name = kubernetes_secret_v1.nginx_htpasswd.metadata[0].name
items {
key = ".htpasswd"
path = ".htpasswd"
}
}
} //httpasswd
volume {
name = "config-folder"
empty_dir {
size_limit = "32Mi"
}
} //config-folder
volume {
name = "ssh-host-ecdsa-key"
secret {
secret_name = kubernetes_secret_v1.sftp_host_keys.metadata[0].name
items {
key = "ssh_host_ecdsa_key"
path = "ssh_host_ecdsa_key"
}
}
} //ssh-host-ecdsa-key
volume {
name = "ssh-host-ecdsa-key-pub"
secret {
secret_name = kubernetes_secret_v1.sftp_host_keys.metadata[0].name
items {
key = "ssh_host_ecdsa_key.pub"
path = "ssh_host_ecdsa_key.pub"
}
}
} //ssh-host-ecdsa-key-pub
volume {
name = "ssh-host-rsa-key"
secret {
secret_name = kubernetes_secret_v1.sftp_host_keys.metadata[0].name
items {
key = "ssh_host_rsa_key"
path = "ssh_host_rsa_key"
}
}
} //ssh-host-rsa-key
volume {
name = "ssh-host-rsa-key-pub"
secret {
secret_name = kubernetes_secret_v1.sftp_host_keys.metadata[0].name
items {
key = "ssh_host_rsa_key.pub"
path = "ssh_host_rsa_key.pub"
}
}
} //ssh-host-rsa-key-pub
}
}
}
}
######
## Services
######
resource "kubernetes_service_v1" "openssh_server" {
metadata {
name = local.sftp_service_name
}
spec {
selector = {
app = kubernetes_deployment_v1.sftp_http_server.metadata.0.name
}
port {
port = local.sftp_ext_port
target_port = local.sftp_ext_port
protocol = "TCP"
}
type = "ClusterIP"
}
}
resource "kubernetes_service_v1" "http_server" {
# Use it if you plan to expose HTTP server via load balancer. If you do not need external connection then remove this line
wait_for_load_balancer = true
metadata {
name = local.http_service_name
}
spec {
selector = {
app = kubernetes_deployment_v1.sftp_http_server.metadata.0.name
}
port {
port = local.http_ext_port
target_port = local.http_ext_port
protocol = "TCP"
}
type = "ClusterIP"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment