feature/ssh-server #1
@@ -1,32 +1,41 @@
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- "docker/**"
|
- "docker/ssh-client/**"
|
||||||
- ".gitea/**"
|
- ".gitea/workflows/ssh-client**"
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 02 * * *"
|
- cron: "0 14 * * 6" #every Saturday at 2:00 PM,
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
build-docker-image:
|
ssh-client:
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ${{gitea.workspace}}docker/ssh-client
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Log in to git.limbosolutions.com docker registry
|
- name: Log in to git.limbosolutions.com container registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: git.limbosolutions.com
|
registry: git.limbosolutions.com
|
||||||
username: ${{ secrets.GITLIMBO_DOCKER_REGISTRY_USERNAME }}
|
username: ${{ secrets.GITLIMBO_DOCKER_REGISTRY_USERNAME }}
|
||||||
password: ${{ secrets.GITLIMBO_DOCKER_REGISTRY_PASSWORD }}
|
password: ${{ secrets.GITLIMBO_DOCKER_REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
- name: Build and push Docker images
|
- name: Build and push ssh-client Docker images
|
||||||
id: push
|
id: push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ${{gitea.workspace}}/docker/Dockerfile
|
file: ${{gitea.workspace}}/docker/ssh-client/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
tags: git.limbosolutions.com/kb/ssh-client
|
tags: git.limbosolutions.com/kb/ssh-client
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
35
.gitea/workflows/ssh-server-build-deploy.yaml
Normal file
35
.gitea/workflows/ssh-server-build-deploy.yaml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "docker/ssh-server/**"
|
||||||
|
- ".gitea/workflows/ssh-server**"
|
||||||
|
schedule:
|
||||||
|
- cron: "0 14 * * 6" #every Saturday at 2:00 PM,
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
ssh-server:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ${{gitea.workspace}}/docker/ssh-server
|
||||||
|
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Log in to git.limbosolutions.com container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.limbosolutions.com
|
||||||
|
username: ${{ secrets.GITLIMBO_DOCKER_REGISTRY_USERNAME }}
|
||||||
|
password: ${{ secrets.GITLIMBO_DOCKER_REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build ssh-server images
|
||||||
|
run: BUILD_ENV=prod ./scripts/build.sh
|
||||||
|
|
||||||
|
- name: Push image
|
||||||
|
run: docker push git.limbosolutions.com/kb/ssh-server
|
||||||
|
|
||||||
|
|
||||||
1
docker/ssh-server/.env
Normal file
1
docker/ssh-server/.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
SSH_PORT="2222"
|
||||||
5
docker/ssh-server/.env.dev
Normal file
5
docker/ssh-server/.env.dev
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#DEBUG_FILE="sshserver.py"
|
||||||
|
#DEBUG_FILE="users.py"
|
||||||
|
SSH_SERVER_ENABLED="true"
|
||||||
|
CONTAINER_TAG="ssh-server:dev"
|
||||||
|
|
||||||
2
docker/ssh-server/.gitignore
vendored
Normal file
2
docker/ssh-server/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
build.env.local
|
||||||
|
.env.local
|
||||||
27
docker/ssh-server/.vscode/launch.json
vendored
Normal file
27
docker/ssh-server/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Python: Remote Attach",
|
||||||
|
"type": "debugpy",
|
||||||
|
"request": "attach",
|
||||||
|
"connect": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 5678
|
||||||
|
},
|
||||||
|
"pathMappings": [
|
||||||
|
{
|
||||||
|
"localRoot": "${workspaceFolder}/app",
|
||||||
|
"remoteRoot": "/app"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"justMyCode": false,
|
||||||
|
"preLaunchTask": "debug"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
26
docker/ssh-server/.vscode/tasks.json
vendored
Normal file
26
docker/ssh-server/.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "debug",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "RUN_ENV=dev ./scripts/run-dev.sh",
|
||||||
|
"problemMatcher": [],
|
||||||
|
"detail": "Runs the container with environment from .env"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "build: debug",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "BUILD_ENV=dev ./scripts/build.sh",
|
||||||
|
"problemMatcher": [],
|
||||||
|
"detail": "Builds docker image with debug requirements"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "build: production",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "./scripts/build.sh",
|
||||||
|
"problemMatcher": [],
|
||||||
|
"detail": "Builds docker image for productions"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
48
docker/ssh-server/Dockerfile
Normal file
48
docker/ssh-server/Dockerfile
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
|
# ┃ 🐳 Dockerfile: Multi-Stage Build for Python + SSH App ┃
|
||||||
|
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||||
|
|
||||||
|
# ──────────────── Stage 1: Base ────────────────
|
||||||
|
# Base image using Debian Bullseye
|
||||||
|
FROM debian:bullseye AS base
|
||||||
|
|
||||||
|
|
||||||
|
# Suppress interactive prompts during package installation
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Install core dependencies:
|
||||||
|
# - Python 3 and pip
|
||||||
|
# - OpenSSH server for remote access
|
||||||
|
# - sudo for privilege escalation
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y \
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
|
openssh-server \
|
||||||
|
curl && \
|
||||||
|
mkdir /var/run/sshd && \
|
||||||
|
apt-get clean
|
||||||
|
|
||||||
|
# Create config directory for app (can be used by dev/prod stages)
|
||||||
|
RUN mkdir -p /etc/app/config
|
||||||
|
|
||||||
|
# ──────────────── Stage 2: Prod ────────────────
|
||||||
|
# Production stage inherits from base
|
||||||
|
FROM base AS prod
|
||||||
|
|
||||||
|
# Copy full app code into image (self-contained)
|
||||||
|
COPY app/ /app
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
|
||||||
|
ENV CONFIGURATION=Production
|
||||||
|
ENV DEBUG=False
|
||||||
|
|
||||||
|
# Default command for production container
|
||||||
|
CMD ["python3", "-u", "/app/main.py"]
|
||||||
|
|
||||||
|
|
||||||
|
#EXPOSE 22
|
||||||
76
docker/ssh-server/Dockerfile.dev
Normal file
76
docker/ssh-server/Dockerfile.dev
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
|
# ┃ 🐳 Dockerfile: Multi-Stage Build for Python + SSH App /(dev) ┃
|
||||||
|
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||||
|
|
||||||
|
# ──────────────── Stage 1: Base ────────────────
|
||||||
|
# Base image using Debian Bullseye
|
||||||
|
FROM debian:bullseye as base
|
||||||
|
|
||||||
|
# Suppress interactive prompts during package installation
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Install core dependencies:
|
||||||
|
# - Python 3 and pip
|
||||||
|
# - OpenSSH server for remote access
|
||||||
|
# - sudo for privilege escalation
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y \
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
|
openssh-server \
|
||||||
|
curl && \
|
||||||
|
mkdir /var/run/sshd && \
|
||||||
|
apt-get clean
|
||||||
|
|
||||||
|
# Create config directory for app (can be used by dev/prod stages)
|
||||||
|
RUN mkdir -p /etc/app/config
|
||||||
|
|
||||||
|
# ──────────────── Stage 2: Dev ────────────────
|
||||||
|
# Development stage inherits from base
|
||||||
|
FROM base AS dev
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Install debugging tools (e.g. debugpy for remote debugging)
|
||||||
|
RUN echo "Installing debug tools..." && \
|
||||||
|
pip install debugpy
|
||||||
|
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install Python dependencies from requirements.txt
|
||||||
|
COPY app/requirements.txt requirements.txt
|
||||||
|
|
||||||
|
# This allows caching of dependencies even if app code is mounted
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
EXPOSE 5678
|
||||||
|
|
||||||
|
|
||||||
|
# runtime environment
|
||||||
|
ENV CONFIGURATION=Debug
|
||||||
|
ENV DEBUG=True
|
||||||
|
ENV SSH_SERVER_ENABLED=False
|
||||||
|
|
||||||
|
# Default command for dev container
|
||||||
|
CMD ["python3","-u", "/app/main.py"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────── Optional: User & SSH Setup ────────────────
|
||||||
|
# Uncomment below to create a non-root user and configure SSH access
|
||||||
|
|
||||||
|
# Create a non-root user with sudo privileges
|
||||||
|
# RUN useradd -m -s /bin/bash devuser && \
|
||||||
|
# echo "devuser:devpass" | chpasswd && \
|
||||||
|
# usermod -aG sudo devuser
|
||||||
|
|
||||||
|
# Configure SSH server to allow password login and specific users
|
||||||
|
# RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin no/' /etc/ssh/sshd_config && \
|
||||||
|
# sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config && \
|
||||||
|
# echo "AllowUsers devuser" >> /etc/ssh/sshd_config
|
||||||
|
|
||||||
|
# Expose SSH port
|
||||||
|
|
||||||
|
|
||||||
50
docker/ssh-server/README.md
Normal file
50
docker/ssh-server/README.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# ssh-server
|
||||||
|
|
||||||
|
## config file example
|
||||||
|
|
||||||
|
``` yaml
|
||||||
|
users:
|
||||||
|
- username: xx
|
||||||
|
password: "123456"
|
||||||
|
public_keys: ## array with public keys
|
||||||
|
- "ssh-ed25519 ssdfdsxvxcsxdfrer"
|
||||||
|
uid: 1002
|
||||||
|
server:
|
||||||
|
options:
|
||||||
|
PermitRootLogin: "no"
|
||||||
|
PasswordAuthentication: "no"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Podman
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
podman pull git.limbosolutions.com/kb/ssh-server:latest
|
||||||
|
|
||||||
|
podman container run \
|
||||||
|
-p 2222:22 \
|
||||||
|
-p 5678:5678 \
|
||||||
|
-v ./local/config:/etc/app/config \
|
||||||
|
-v ./local/server-certs:/etc/ssh/certs \
|
||||||
|
-v ./local/home:/home \
|
||||||
|
git.limbosolutions.com/kb/ssh-server:latest
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## docker
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
docker pull git.limbosolutions.com/kb/ssh-server:latest
|
||||||
|
|
||||||
|
docker container run \
|
||||||
|
-p 2222:22 \
|
||||||
|
-p 5678:5678 \
|
||||||
|
-v ./local/config:/etc/app/config \
|
||||||
|
-v ./local/server-certs:/etc/ssh/certs \
|
||||||
|
-v ./local/home:/home \
|
||||||
|
git.limbosolutions.com/kb/ssh-server:latest
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## dev and testing
|
||||||
|
|
||||||
|
Using vscode, check .vscode folder build and debug tasks as launch settings.
|
||||||
1
docker/ssh-server/app/.gitignore
vendored
Normal file
1
docker/ssh-server/app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
__pycache__
|
||||||
28
docker/ssh-server/app/globals.py
Normal file
28
docker/ssh-server/app/globals.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import os
|
||||||
|
import yaml
|
||||||
|
def is_debugging(): return os.getenv("CONFIGURATION", "").lower() == "debug"
|
||||||
|
|
||||||
|
|
||||||
|
file_path="/etc/app/config/config.yaml"
|
||||||
|
config=None
|
||||||
|
|
||||||
|
def config_exits():
|
||||||
|
return get_config() is not None
|
||||||
|
|
||||||
|
def sshserver_enabled():
|
||||||
|
return not is_debugging() or os.getenv("SSH_SERVER_ENABLED", "false").lower() == "true"
|
||||||
|
|
||||||
|
def get_config():
|
||||||
|
global config
|
||||||
|
if config == None:
|
||||||
|
load_config()
|
||||||
|
return config
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
global config
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
config = yaml.safe_load(f)
|
||||||
|
else:
|
||||||
|
print(f"⚠️ missing " + file_path)
|
||||||
|
|
||||||
41
docker/ssh-server/app/init.py
Normal file
41
docker/ssh-server/app/init.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import globals
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
if globals.is_debugging():
|
||||||
|
import debugpy
|
||||||
|
debugpy.listen(("0.0.0.0", 5678))
|
||||||
|
print("INFO: Waiting for debugger to attach...")
|
||||||
|
debugpy.wait_for_client()
|
||||||
|
print("INFO: Debugger attached")
|
||||||
|
else:
|
||||||
|
print("👉 starting setup")
|
||||||
|
|
||||||
|
setup_container()
|
||||||
|
|
||||||
|
if globals.is_debugging():
|
||||||
|
debug_file = os.getenv("DEBUG_FILE", "")
|
||||||
|
if debug_file and os.path.isfile(debug_file):
|
||||||
|
print(f"🔍 Running debug file: {debug_file}")
|
||||||
|
subprocess.run(["python3", debug_file], check=True, stdout=sys.stdout, stderr=sys.stderr)
|
||||||
|
|
||||||
|
return False
|
||||||
|
elif debug_file:
|
||||||
|
print(f"⚠️ DEBUG_FILE set but file not found: {debug_file}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def setup_container():
|
||||||
|
marker_file = "/var/run/container_initialized"
|
||||||
|
if not os.path.exists(marker_file):
|
||||||
|
print("INFO: First-time setup running...")
|
||||||
|
# 👉 Your one-time setup logic here
|
||||||
|
|
||||||
|
# Create the marker file
|
||||||
|
with open(marker_file, "w") as f:
|
||||||
|
f.write("initialized\n")
|
||||||
|
else:
|
||||||
|
print("TRACE: Already initialized. Skipping setup.")
|
||||||
|
|
||||||
14
docker/ssh-server/app/main.py
Normal file
14
docker/ssh-server/app/main.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import globals
|
||||||
|
import subprocess
|
||||||
|
import init
|
||||||
|
import users
|
||||||
|
import sshserver
|
||||||
|
|
||||||
|
if init.main():
|
||||||
|
users.load()
|
||||||
|
sshserver.load()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
1
docker/ssh-server/app/requirements.txt
Normal file
1
docker/ssh-server/app/requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
PyYAML>=5.4
|
||||||
126
docker/ssh-server/app/sshserver.py
Normal file
126
docker/ssh-server/app/sshserver.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import yaml
|
||||||
|
import subprocess
|
||||||
|
import crypt
|
||||||
|
import os
|
||||||
|
import globals
|
||||||
|
import sys
|
||||||
|
|
||||||
|
config_file_path='/etc/ssh/sshd_config'
|
||||||
|
|
||||||
|
def set_sshd_option(file_path: str, key: str, value: str) -> None:
|
||||||
|
updated = False
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
if line.strip().startswith(key):
|
||||||
|
lines.append(f"{key} {value}\n")
|
||||||
|
updated = True
|
||||||
|
else:
|
||||||
|
lines.append(line)
|
||||||
|
|
||||||
|
if not updated:
|
||||||
|
lines.append(f"{key} {value}\n")
|
||||||
|
|
||||||
|
with open(file_path, 'w') as f:
|
||||||
|
f.writelines(lines)
|
||||||
|
|
||||||
|
print(f"✅ Updated {key} to '{value}' in {file_path}")
|
||||||
|
|
||||||
|
def load():
|
||||||
|
setup()
|
||||||
|
#print_server_config()
|
||||||
|
if globals.sshserver_enabled():
|
||||||
|
start_server()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def setup_certs():
|
||||||
|
certs=[
|
||||||
|
"/etc/ssh/certs/ssh_host_rsa_key",
|
||||||
|
"/etc/ssh/certs/ssh_host_ecdsa_key",
|
||||||
|
"/etc/ssh/certs/ssh_host_ed25519_key"
|
||||||
|
]
|
||||||
|
if not os.path.exists("/etc/ssh/certs"):
|
||||||
|
os.makedirs("/etc/ssh/certs")
|
||||||
|
print(f"📁 Created folder: /etc/ssh/certs")
|
||||||
|
|
||||||
|
if not os.listdir("/etc/ssh/certs"):
|
||||||
|
subprocess.run([
|
||||||
|
"ssh-keygen", "-t", "rsa", "-f",
|
||||||
|
"/etc/ssh/certs/ssh_host_rsa_key"
|
||||||
|
], check=True, stdout=sys.stdout, stderr=sys.stderr)
|
||||||
|
print(f"✅ RSA key and certificate created:🔑 /etc/ssh/certs/ssh_host_rsa_key")
|
||||||
|
|
||||||
|
subprocess.run([
|
||||||
|
"ssh-keygen", "-t", "ecdsa", "-f",
|
||||||
|
"/etc/ssh/certs/ssh_host_ecdsa_key"
|
||||||
|
], check=True, stdout=sys.stdout, stderr=sys.stderr)
|
||||||
|
print(f"✅ RSA key and certificate created:🔑 /etc/ssh/certs/ssh_host_ecdsa_key")
|
||||||
|
|
||||||
|
subprocess.run([
|
||||||
|
"ssh-keygen", "-t", "ed25519", "-f",
|
||||||
|
"/etc/ssh/certs/ssh_host_ed25519_key"
|
||||||
|
], check=True, stdout=sys.stdout, stderr=sys.stderr)
|
||||||
|
print(f"✅ RSA key and certificate created:🔑 /etc/ssh/certs/ssh_host_ed25519_key")
|
||||||
|
|
||||||
|
certLines=[]
|
||||||
|
for cert in certs:
|
||||||
|
if os.path.exists(cert):
|
||||||
|
certLines.append(f"HostKey {cert}\n")
|
||||||
|
else:
|
||||||
|
print(f"❌ HostKey path not found {cert}")
|
||||||
|
if not certLines: RuntimeError("❌ Missing server certificates configuration. Bind Volume to /etc/ssh/certs")
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
with open(config_file_path, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
if line.strip().startswith("HostKey"):
|
||||||
|
continue # remove existing HostKey lines
|
||||||
|
lines.append(line)
|
||||||
|
|
||||||
|
for key in certLines:
|
||||||
|
print(f"✅ HostKey path updated to use {key}")
|
||||||
|
lines.append(key)
|
||||||
|
|
||||||
|
with open(config_file_path, 'w') as f:
|
||||||
|
f.writelines(lines)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def setup():
|
||||||
|
global config_file_path
|
||||||
|
|
||||||
|
serverConfig = globals.get_config().get("server") if globals.config_exits() else None
|
||||||
|
|
||||||
|
if not serverConfig:
|
||||||
|
return
|
||||||
|
|
||||||
|
optionsConfig = serverConfig.get("options")
|
||||||
|
if optionsConfig:
|
||||||
|
for option in optionsConfig:
|
||||||
|
set_sshd_option(config_file_path, option, optionsConfig[option])
|
||||||
|
setup_certs()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def print_server_config():
|
||||||
|
with open(config_file_path, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
print(content)
|
||||||
|
|
||||||
|
|
||||||
|
def start_server():
|
||||||
|
print("INFO: Starting ssh server.")
|
||||||
|
serverPort=None
|
||||||
|
serverConfig = globals.get_config().get("server") if globals.config_exits() else None
|
||||||
|
if serverConfig:
|
||||||
|
serverPort = serverConfig.get("port")
|
||||||
|
if serverPort:
|
||||||
|
subprocess.run(["/usr/sbin/sshd", "-D", "-e", "-p", str(serverPort)])
|
||||||
|
else:
|
||||||
|
subprocess.run(["/usr/sbin/sshd", "-D", "-e"])
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
load()
|
||||||
89
docker/ssh-server/app/users.py
Normal file
89
docker/ssh-server/app/users.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import yaml
|
||||||
|
import subprocess
|
||||||
|
import crypt
|
||||||
|
import os
|
||||||
|
import globals
|
||||||
|
import pwd
|
||||||
|
# users:
|
||||||
|
# - username: alice
|
||||||
|
# authorized_keys: publich ssh key
|
||||||
|
# - username: bob
|
||||||
|
# password: hunter2
|
||||||
|
|
||||||
|
|
||||||
|
def user_exists(username):
|
||||||
|
try:
|
||||||
|
pwd.getpwnam(username)
|
||||||
|
return True
|
||||||
|
except KeyError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def create_user(uid, username ,password, shell="/bin/bash"):
|
||||||
|
|
||||||
|
if not shell: shell = "/bin/bash"
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
return
|
||||||
|
|
||||||
|
useradd_cmd = [
|
||||||
|
'useradd',
|
||||||
|
'-m',
|
||||||
|
'-s', shell,
|
||||||
|
]
|
||||||
|
|
||||||
|
if uid: useradd_cmd.append("-u " + str(uid))
|
||||||
|
if password: useradd_cmd.append("-p" + crypt.crypt(password, crypt.mksalt(crypt.METHOD_SHA512)))
|
||||||
|
useradd_cmd.append(username)
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(useradd_cmd
|
||||||
|
, check=True)
|
||||||
|
print(f"✅ User '{username}' created with shell '{shell}' and password.")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"❌ Failed to create user '{username}': {e}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def setup_ssh(username, public_key):
|
||||||
|
ssh_dir = f"/home/{username}/.ssh"
|
||||||
|
auth_keys = os.path.join(ssh_dir, "authorized_keys")
|
||||||
|
uid = pwd.getpwnam(username).pw_uid
|
||||||
|
gid = pwd.getpwnam(username).pw_gid
|
||||||
|
|
||||||
|
os.makedirs(ssh_dir, mode=0o700, exist_ok=True)
|
||||||
|
|
||||||
|
# Check if key already exists
|
||||||
|
key_exists = False
|
||||||
|
if os.path.exists(auth_keys):
|
||||||
|
with open(auth_keys, "r") as f:
|
||||||
|
existing_keys = f.read().splitlines()
|
||||||
|
key_exists = public_key.strip() in existing_keys
|
||||||
|
|
||||||
|
if not key_exists:
|
||||||
|
with open(auth_keys, "a") as f:
|
||||||
|
f.write(public_key.strip() + "\n")
|
||||||
|
print(f"🔐 SSH key added for '{username}'.")
|
||||||
|
else:
|
||||||
|
print(f"⚠️ SSH key already exists for '{username}'. Skipping.")
|
||||||
|
|
||||||
|
os.chmod(ssh_dir, 0o700)
|
||||||
|
os.chmod(auth_keys, 0o600)
|
||||||
|
os.chown(ssh_dir, uid, gid)
|
||||||
|
os.chown(auth_keys, uid, gid)
|
||||||
|
|
||||||
|
def load():
|
||||||
|
users = globals.get_config()["users"] if globals.config_exits() else None
|
||||||
|
if users:
|
||||||
|
for user in users:
|
||||||
|
if not user_exists(user.get('username')):
|
||||||
|
create_user(user.get('uid'), user.get('username'),user.get('password'), user.get('shell'))
|
||||||
|
if user.get('public_keys'):
|
||||||
|
for public_key in user.get('public_keys'):
|
||||||
|
setup_ssh(user.get('username'), public_key)
|
||||||
|
else:
|
||||||
|
print(f"⚠️ missing users configuration")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
load()
|
||||||
2
docker/ssh-server/build.env
Normal file
2
docker/ssh-server/build.env
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
BUILD_ENV_IMAGE_TAG="git.limbosolutions.com/kb/ssh-server:latest"
|
||||||
|
BUILD_CLI="docker"
|
||||||
3
docker/ssh-server/build.env.dev
Normal file
3
docker/ssh-server/build.env.dev
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
BUILD_ENV_IMAGE_TAG="ssh-server:dev"
|
||||||
|
BUILD_ENV_DOCKER_FILE="Dockerfile.dev"
|
||||||
|
BUILD_CLI="podman"
|
||||||
92
docker/ssh-server/scripts/build.sh
Executable file
92
docker/ssh-server/scripts/build.sh
Executable file
@@ -0,0 +1,92 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
|
# ┃ 🐳 Podman Build Script with Layered .env Configuration ┃
|
||||||
|
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||||
|
#
|
||||||
|
# This script builds a Podman image using a specified Dockerfile,
|
||||||
|
# dynamically injecting build-time environment variables from a
|
||||||
|
# prioritized set of `.env` files.
|
||||||
|
#
|
||||||
|
# ──────────────── Behavior ────────────────
|
||||||
|
# - Loads variables from the following files (in order):
|
||||||
|
# build.env
|
||||||
|
# build.env.${BUILD_ENV}
|
||||||
|
# build.local.env
|
||||||
|
#
|
||||||
|
# - Later files override earlier ones (last-write-wins)
|
||||||
|
# - Variables starting with `BUILD_ENV` are excluded from build args
|
||||||
|
#
|
||||||
|
# ──────────────── Required Environment Variables ────────────────
|
||||||
|
# - BUILD_ENV: (optional) Environment name (e.g. dev, prod)
|
||||||
|
# - BUILD_ENV_DOCKER_FILE: Dockerfile to use (default: Dockerfile)
|
||||||
|
# - BUILD_ENV_IMAGE_TAG: Tag to assign to the built image
|
||||||
|
#
|
||||||
|
# ──────────────── Example Usage ────────────────
|
||||||
|
# BUILD_ENV=dev BUILD_ENV_IMAGE_TAG=myapp:dev ./build.sh
|
||||||
|
# BUILD_ENV=prod BUILD_ENV_DOCKER_FILE=Dockerfile.prod BUILD_ENV_IMAGE_TAG=myapp:prod ./build.sh
|
||||||
|
#
|
||||||
|
# ──────────────── Output ────────────────
|
||||||
|
# - Logs each loaded file
|
||||||
|
# - Displays the final build command
|
||||||
|
# - Executes the podman build with all resolved --build-arg flags
|
||||||
|
#
|
||||||
|
# Author: Márcio
|
||||||
|
# Last Updated: September 2025
|
||||||
|
|
||||||
|
POSSIBLE_BUILD_ARGS_FILES="\
|
||||||
|
build.env \
|
||||||
|
"
|
||||||
|
|
||||||
|
|
||||||
|
if [ -n "${BUILD_ENV+x}" ]; then
|
||||||
|
POSSIBLE_BUILD_ARGS_FILES="$POSSIBLE_BUILD_ARGS_FILES \
|
||||||
|
build.env.${BUILD_ENV} \
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
|
||||||
|
POSSIBLE_BUILD_ARGS_FILES="$POSSIBLE_BUILD_ARGS_FILES \
|
||||||
|
build.env.local"
|
||||||
|
|
||||||
|
|
||||||
|
# load variables into this context
|
||||||
|
declare -A build_args_map
|
||||||
|
BUILD_ARGS=""
|
||||||
|
|
||||||
|
for file in $POSSIBLE_BUILD_ARGS_FILES; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
echo "🔧 Loading variables from $file"
|
||||||
|
set -a
|
||||||
|
source "$file"
|
||||||
|
set +a
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
# Skip comments and empty lines
|
||||||
|
[[ "$line" =~ ^#.*$ || -z "$line" ]] && continue
|
||||||
|
|
||||||
|
# Extract key and value
|
||||||
|
key="$(echo "$line" | cut -d= -f1 | xargs)"
|
||||||
|
value="$(echo "$line" | cut -d= -f2- | xargs)"
|
||||||
|
|
||||||
|
# Skip keys that start with BUILD_ENV
|
||||||
|
if [[ "$key" == BUILD_ENV* ]]; then
|
||||||
|
echo "Skipping key starting with BUILD_ENV: $key"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
build_args_map["$key"]="$value"
|
||||||
|
done < "$file"
|
||||||
|
else
|
||||||
|
echo "⚠️ Skipping missing file: $file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for key in "${!build_args_map[@]}"; do
|
||||||
|
BUILD_ARGS+=" --build-arg $key=${build_args_map[$key]}"
|
||||||
|
done
|
||||||
|
# sets default docker file
|
||||||
|
BUILD_ENV_DOCKER_FILE="${BUILD_ENV_DOCKER_FILE:-Dockerfile}"
|
||||||
|
|
||||||
|
echo "Build env: $BUILD_ENV"
|
||||||
|
echo "Build DockerFile: $BUILD_ENV_DOCKER_FILE"
|
||||||
|
echo "Build Tag: $BUILD_ENV_IMAGE_TAG"
|
||||||
|
echo "Running: $BUILD_CLI -f $BUILD_ENV_DOCKER_FILE $BUILD_ARGS -t $BUILD_ENV_IMAGE_TAG ."
|
||||||
|
$BUILD_CLI build -f $BUILD_ENV_DOCKER_FILE $BUILD_ARGS -t $BUILD_ENV_IMAGE_TAG .
|
||||||
|
|
||||||
40
docker/ssh-server/scripts/run-dev.sh
Executable file
40
docker/ssh-server/scripts/run-dev.sh
Executable file
@@ -0,0 +1,40 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
RUN_ENV=dev
|
||||||
|
|
||||||
|
POSSIBLE_ENV_FILES="\
|
||||||
|
.env
|
||||||
|
"
|
||||||
|
|
||||||
|
if [ -n "${RUN_ENV+x}" ]; then
|
||||||
|
POSSIBLE_ENV_FILES="$POSSIBLE_ENV_FILES \
|
||||||
|
.env.${RUN_ENV} \
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
|
||||||
|
POSSIBLE_ENV_FILES="$POSSIBLE_ENV_FILES \
|
||||||
|
.env.local"
|
||||||
|
|
||||||
|
for file in $POSSIBLE_ENV_FILES; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
echo "🔧 Loading variables from $file"
|
||||||
|
set -a
|
||||||
|
source "$file"
|
||||||
|
set +a
|
||||||
|
else
|
||||||
|
echo "⚠️ Skipping missing file: $file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
# Ignored if uing dev docker file:
|
||||||
|
# - DEBUG_FILE
|
||||||
|
# - SSH_SERVER_ENABLED
|
||||||
|
podman container run -d --rm \
|
||||||
|
-e DEBUG_FILE="${DEBUG_FILE}" \
|
||||||
|
-e SSH_SERVER_ENABLED="${SSH_SERVER_ENABLED:-false}" \
|
||||||
|
-p 2222:22 \
|
||||||
|
-p 5678:5678 \
|
||||||
|
-v ./app:/app \
|
||||||
|
-v ./local/home:/home \
|
||||||
|
-v ./local/config:/etc/app/config \
|
||||||
|
-v ./local/server-certs:/etc/ssh/certs \
|
||||||
|
${CONTAINER_TAG}
|
||||||
|
|
||||||
Reference in New Issue
Block a user