From 071b4eea9eec0e46339494f0812c92722d076327 Mon Sep 17 00:00:00 2001 From: Alexandre Pires Date: Thu, 22 May 2025 22:55:27 +0200 Subject: [PATCH] feat(ssh_locker): implement ssh locker helper scripts and configuration for Duo API integration --- bin/occ | 3 - bin/ssh_locker | 15 ++ bin/ssh_locker_helper | 27 +++ sectool.env | 1 + terraform/k8sapps/apps.tf | 11 + terraform/k8sapps/resources/alexpires.me.conf | 11 + terraform/k8sapps/secrets.tf | 16 ++ terraform/k8sapps/sectool.env | 3 + .../modules/apps/ssh_locker_web/config.tf | 14 ++ terraform/modules/apps/ssh_locker_web/main.tf | 191 ++++++++++++++++++ .../modules/apps/ssh_locker_web/outputs.tf | 7 + .../modules/apps/ssh_locker_web/provider.tf | 9 + .../modules/apps/ssh_locker_web/variables.tf | 55 +++++ 13 files changed, 360 insertions(+), 3 deletions(-) delete mode 100755 bin/occ create mode 100755 bin/ssh_locker create mode 100755 bin/ssh_locker_helper create mode 100644 terraform/modules/apps/ssh_locker_web/config.tf create mode 100644 terraform/modules/apps/ssh_locker_web/main.tf create mode 100644 terraform/modules/apps/ssh_locker_web/outputs.tf create mode 100644 terraform/modules/apps/ssh_locker_web/provider.tf create mode 100644 terraform/modules/apps/ssh_locker_web/variables.tf diff --git a/bin/occ b/bin/occ deleted file mode 100755 index b0b128d..0000000 --- a/bin/occ +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -POD_NAME=$(kubectl get pods -n nextcloud -l app=nextcloud -o jsonpath="{.items[0].metadata.name}") -kubectl -n nextcloud exec -it $POD_NAME -- /bin/su -s /bin/bash www-data -c "/var/www/html/occ $*" \ No newline at end of file diff --git a/bin/ssh_locker b/bin/ssh_locker new file mode 100755 index 0000000..481ec9c --- /dev/null +++ b/bin/ssh_locker @@ -0,0 +1,15 @@ +#bin/bash + +function usage() { + echo "Usage: ssh_locker_helper " + echo "Actions:" + echo " lock - Lock the SSH locker" + echo " unlock - Unlock the SSH locker" +} + +if [ -z "$1" ] || [ -z "$2" ]; then + usage + exit 1 +fi + +sectool exec bash -c "bin/ssh_locker_helper $1 $2" \ No newline at end of file diff --git a/bin/ssh_locker_helper b/bin/ssh_locker_helper new file mode 100755 index 0000000..dc082e3 --- /dev/null +++ b/bin/ssh_locker_helper @@ -0,0 +1,27 @@ +#/!/bin/bash +function usage() { + echo "Usage: ssh_locker_helper " + echo "Actions:" + echo " lock - Lock the SSH locker" + echo " unlock - Unlock the SSH locker" +} +if [ -z "$SERVER_API_ACCESS_TOKEN" ]; then + echo "Please set the SERVER_API_ACCESS_TOKEN environment variable." + exit 1 +fi +if [ -z "$1" ] || [ -z "$2" ]; then + usage + exit 1 +fi + +response=$(curl -s -o /dev/null -w "%{redirect_url}" \ + -X POST https://alexpires.me/api/ssh_locker/action \ + -H "Content-Type: application/json" \ + -H "X-Auth-Token: $SERVER_API_ACCESS_TOKEN" \ + -d '{"user":"'$1'","action":"'$2'"}') + +if [ -n "$response" ]; then + xdg-open "$response" +else + echo "No redirect URL received." +fi \ No newline at end of file diff --git a/sectool.env b/sectool.env index 99bdb5e..5fa926a 100644 --- a/sectool.env +++ b/sectool.env @@ -8,3 +8,4 @@ SCW_ACCESS_KEY=$SCALEWAY_ACCESS_KEY SCW_SECRET_KEY=$SCALEWAY_SECRET_KEY SCW_DEFAULT_PROJECT_ID=$SCALEWAY_PROJECT_ID SCW_DEFAULT_ORGANIZATION_ID=$SCALEWAY_ORGANIZATION_ID +SERVER_API_ACCESS_TOKEN=$SERVER_API_ACCESS_TOKEN \ No newline at end of file diff --git a/terraform/k8sapps/apps.tf b/terraform/k8sapps/apps.tf index de9ee1a..0e82578 100644 --- a/terraform/k8sapps/apps.tf +++ b/terraform/k8sapps/apps.tf @@ -128,3 +128,14 @@ module "trivy" { from_email = "trivy@${local.primary_domain}" to_email = "c.alexandre.pires@${local.primary_domain}" } + +module "ssh_locker_web" { + source = "../modules/apps/ssh_locker_web" + + duo_client_id = var.server_api_duo_client_id + duo_client_secret = var.server_api_duo_client_secret + access_token = var.server_api_access_token + duo_api_host = "api-7cda1654.duosecurity.com" + redirect_uri = "https://alexpires.me/api/ssh_locker/duo-callback" + socket_path = "/var/run/ssh_locker/provision.sock" +} diff --git a/terraform/k8sapps/resources/alexpires.me.conf b/terraform/k8sapps/resources/alexpires.me.conf index 9b784c2..0da8286 100644 --- a/terraform/k8sapps/resources/alexpires.me.conf +++ b/terraform/k8sapps/resources/alexpires.me.conf @@ -9,3 +9,14 @@ location ^~ /config/dav { proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; } + +location ^~ /api/ssh_locker/ { + rewrite ^/api/ssh_locker/(.*)$ /$1 break; + proxy_pass https://backend.ssh-locker-web.svc.cluster.local; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; +} \ No newline at end of file diff --git a/terraform/k8sapps/secrets.tf b/terraform/k8sapps/secrets.tf index 7fecddf..269a0e4 100644 --- a/terraform/k8sapps/secrets.tf +++ b/terraform/k8sapps/secrets.tf @@ -106,3 +106,19 @@ variable "gitea_jwt_secret" { type = string sensitive = true } + +variable "server_api_duo_client_id" { + description = "Duo client ID" + type = string + sensitive = true +} +variable "server_api_duo_client_secret" { + description = "Duo client secret" + type = string + sensitive = true +} +variable "server_api_access_token" { + description = "Access token for ssh_locker API" + type = string + sensitive = true +} diff --git a/terraform/k8sapps/sectool.env b/terraform/k8sapps/sectool.env index 3123310..b3fe89c 100644 --- a/terraform/k8sapps/sectool.env +++ b/terraform/k8sapps/sectool.env @@ -28,3 +28,6 @@ TF_VAR_gitea_lfs_jwt_secret=$GITEA_LFS_JWT_SECRET TF_VAR_gitea_secret_key=$GITEA_SECRET_KEY TF_VAR_gitea_internal_token=$GITEA_INTERNAL_TOKEN TF_VAR_gitea_jwt_secret=$GITEA_JWT_SECRET +TF_VAR_server_api_duo_client_id=$SERVER_API_DUO_CLIENT_ID +TF_VAR_server_api_duo_client_secret=$SERVER_API_DUO_CLIENT_SECRET +TF_VAR_server_api_access_token=$SERVER_API_ACCESS_TOKEN \ No newline at end of file diff --git a/terraform/modules/apps/ssh_locker_web/config.tf b/terraform/modules/apps/ssh_locker_web/config.tf new file mode 100644 index 0000000..a944ffa --- /dev/null +++ b/terraform/modules/apps/ssh_locker_web/config.tf @@ -0,0 +1,14 @@ +locals { + config = { + "clientId" : var.duo_client_id + "clientSecret" : var.duo_client_secret + "apiHost" : var.duo_api_host + "redirectUri" : var.redirect_uri + "accessToken" : var.access_token + "socketPath" : "/app/socket" + "tlsCert" : var.tls_enabled ? "/etc/ssl/certs/private/tls.crt" : "" + "tlsKey" : var.tls_enabled ? "/etc/ssl/certs/private/tls.key" : "" + } + config_json = jsonencode(local.config) + config_checksum = sha256(local.config_json) +} diff --git a/terraform/modules/apps/ssh_locker_web/main.tf b/terraform/modules/apps/ssh_locker_web/main.tf new file mode 100644 index 0000000..a378a5b --- /dev/null +++ b/terraform/modules/apps/ssh_locker_web/main.tf @@ -0,0 +1,191 @@ +resource "kubernetes_namespace" "ssh_locker_web" { + metadata { + name = "ssh-locker-web" + } +} + +resource "kubernetes_secret" "ssh_locker_web" { + metadata { + name = "ssh-locker-web-config" + namespace = kubernetes_namespace.ssh_locker_web.metadata[0].name + } + data = { + "config.json" = local.config_json + } + type = "Opaque" +} + +resource "kubernetes_service_account" "ssh_locker_web" { + metadata { + name = "ssh-locker-web" + namespace = kubernetes_namespace.ssh_locker_web.metadata[0].name + } + + automount_service_account_token = false +} + +resource "kubernetes_manifest" "internal_cert" { + count = var.tls_enabled ? 1 : 0 + manifest = { + apiVersion = "cert-manager.io/v1" + kind = "Certificate" + metadata = { + name = "internal-cert" + namespace = kubernetes_namespace.ssh_locker_web.metadata[0].name + } + spec = { + secretName = "internal-cert" + issuerRef = { + name = var.issuer + kind = "ClusterIssuer" + } + commonName = "backend.ssh-locker-web.svc.cluster.local" + dnsNames = ["backend.ssh-locker-web.svc.cluster.local"] + } + } +} +resource "kubernetes_deployment" "ssh_locker_web" { + metadata { + name = "backend" + namespace = kubernetes_namespace.ssh_locker_web.metadata[0].name + labels = { + app = "backend" + } + annotations = { + "config/checksum" = local.config_checksum + } + } + + spec { + replicas = 1 + + selector { + match_labels = { + app = "backend" + } + } + + template { + metadata { + labels = { + app = "backend" + } + } + + spec { + security_context { + run_as_user = var.user_id + run_as_group = var.group_id + fs_group = var.group_id + run_as_non_root = true + } + container { + name = "backend" + image = "a13labs/ssh_locker_web:latest" + + security_context { + allow_privilege_escalation = false + } + + port { + container_port = var.tls_enabled ? 8443 : 8080 + name = var.tls_enabled ? "https" : "http" + } + + readiness_probe { + tcp_socket { + port = var.tls_enabled ? 8443 : 8080 + } + } + + liveness_probe { + tcp_socket { + port = var.tls_enabled ? 8443 : 8080 + } + } + + volume_mount { + name = "config-volume" + mount_path = "/app/config.json" + sub_path = "config.json" + read_only = true + } + + volume_mount { + name = "socket-volume" + mount_path = "/app/socket" + } + + dynamic "volume_mount" { + for_each = var.tls_enabled ? [1] : [] + content { + name = "internal-cert" + mount_path = "/etc/ssl/certs/private" + read_only = true + } + + } + } + + volume { + name = "config-volume" + secret { + secret_name = kubernetes_secret.ssh_locker_web.metadata[0].name + items { + key = "config.json" + path = "config.json" + } + } + } + + volume { + name = "socket-volume" + host_path { + path = var.socket_path + } + } + + dynamic "volume" { + for_each = var.tls_enabled ? [1] : [] + content { + name = "internal-cert" + secret { + secret_name = "internal-cert" + items { + key = "tls.crt" + path = "tls.crt" + } + items { + key = "tls.key" + path = "tls.key" + } + } + } + } + + service_account_name = kubernetes_service_account.ssh_locker_web.metadata[0].name + } + } + } +} + +resource "kubernetes_service" "backend" { + metadata { + name = "backend" + namespace = kubernetes_namespace.ssh_locker_web.metadata[0].name + } + + spec { + selector = { + app = "backend" + } + + port { + name = var.tls_enabled ? "https" : "http" + port = var.tls_enabled ? 443 : 80 + target_port = var.tls_enabled ? 8443 : 8080 + } + + type = "ClusterIP" + } +} diff --git a/terraform/modules/apps/ssh_locker_web/outputs.tf b/terraform/modules/apps/ssh_locker_web/outputs.tf new file mode 100644 index 0000000..5aa21ad --- /dev/null +++ b/terraform/modules/apps/ssh_locker_web/outputs.tf @@ -0,0 +1,7 @@ +output "namespace" { + value = kubernetes_namespace.ssh_locker_web.metadata[0].name +} + +output "app_name" { + value = "ssh_locker_web" +} diff --git a/terraform/modules/apps/ssh_locker_web/provider.tf b/terraform/modules/apps/ssh_locker_web/provider.tf new file mode 100644 index 0000000..3c8e1a0 --- /dev/null +++ b/terraform/modules/apps/ssh_locker_web/provider.tf @@ -0,0 +1,9 @@ +terraform { + required_version = "~>1.8" + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = "~>2.18" + } + } +} diff --git a/terraform/modules/apps/ssh_locker_web/variables.tf b/terraform/modules/apps/ssh_locker_web/variables.tf new file mode 100644 index 0000000..2391914 --- /dev/null +++ b/terraform/modules/apps/ssh_locker_web/variables.tf @@ -0,0 +1,55 @@ +variable "duo_client_id" { + description = "Duo client ID" + type = string + sensitive = true +} + +variable "duo_client_secret" { + description = "Duo client secret" + type = string + sensitive = true +} + +variable "duo_api_host" { + description = "Duo API host" + type = string +} + +variable "redirect_uri" { + description = "Redirect URI for Duo authentication" + type = string +} + +variable "access_token" { + description = "Access token for Duo API" + type = string + sensitive = true +} + +variable "socket_path" { + description = "Socket path for Duo API" + type = string +} + +variable "user_id" { + description = "User for running the pod" + type = number + default = 1000 +} + +variable "group_id" { + description = "Group for running the pod" + type = number + default = 1000 +} + +variable "issuer" { + description = "The issuer for the certificate" + type = string + default = "internal-ca" +} +variable "tls_enabled" { + description = "Enable TLS for the SFTPGo server" + type = bool + default = true +}