From 34420297331d6f709d8af598f0a6b2ef84fdc6fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Lov=C3=A9n?= Date: Sat, 4 Sep 2021 22:35:39 +0200 Subject: [PATCH] Initial commit --- .gitignore | 6 ++ README.md | 27 ++++++ build/Dockerfile | 10 ++ build/entrypoint.sh | 227 ++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yaml | 22 +++++ 5 files changed, 292 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build/Dockerfile create mode 100755 build/entrypoint.sh create mode 100644 docker-compose.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..05de021 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +**/* +!.gitignore +!README.md +!docker-compose.yaml +!build/ +!build/* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..170a983 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# SSH entrypoint with yubikey OTP support + +This docker container runs a sshd instance which is exposed through TCP 443. It can be a nice and secure way into your network. + +Since this is the only TCP service I expose, traefik handles this automagically while also routing SSL HTTPS traffic the normal way. + +The image is a modified version of https://github.com/Hermsi1337/docker-sshd which has been made to work with yubikey OTP certification and allow for personalized `.ssh/config` files to be loaded. + +## ENV variable `SSH_USERS` + +`SSH_USERS` contain a comma separates lists of username:UID:GUI that will be allowed to login. + +Ex: + +`SSH_USERS=myuser:1000:1000,anotheruser:1001:1001` + +The directory mapped to `/conf.d/authorized_keys` contain files for authorized_keys, authorized yubikeys and ssh config. + +- A file named `myuser` will be copied to `/home/myuser/.ssh/authorized_keys` +- A file named `myuser.config` will be copied to `/home/myuser/.ssh/config` +- A file name `myuser.yubi` will be copied to `/home/myuser/.yubico/authorized_yubikeys` + +The format of the `.yubi` file is your username followed by a list of the first 12 characters from any OTP from all of your yubikeys, all separated by `:`s. E.g.: + +```yaml +myuser:cccccccgklgc:ccccccclabca: +``` \ No newline at end of file diff --git a/build/Dockerfile b/build/Dockerfile new file mode 100644 index 0000000..e9e2d8f --- /dev/null +++ b/build/Dockerfile @@ -0,0 +1,10 @@ +FROM hermsi/alpine-sshd:latest + +RUN apk add --no-cache \ + openssh-server-pam \ + yubico-pam \ + google-authenticator \ + && \ + rm -rf /var/cache/apk/* + +COPY entrypoint.sh / \ No newline at end of file diff --git a/build/entrypoint.sh b/build/entrypoint.sh new file mode 100755 index 0000000..b64b36d --- /dev/null +++ b/build/entrypoint.sh @@ -0,0 +1,227 @@ +#!/usr/bin/env bash + +############################################# +# This file is copied from https://github.com/Hermsi1337/docker-sshd +# Two additions have been made to allow for yubikey authentication +# and adding an .ssh/config file +############################################# + +set -e + +# enable debug mode if desired +if [[ "${DEBUG}" == "true" ]]; then + set -x +fi + +log() { + LEVEL="${1}" + TO_LOG="${2}" + + WHITE='\033[1;37m' + YELLOW='\033[1;33m' + RED='\033[1;31m' + NO_COLOR='\033[0m' + + if [[ "${LEVEL}" == "warning" ]]; then + LOG_LEVEL="${YELLOW}WARN${NO_COLOR}" + elif [[ "${LEVEL}" == "error" ]]; then + LOG_LEVEL="${RED}ERROR${NO_COLOR}" + else + LOG_LEVEL="${WHITE}INFO${NO_COLOR}" + if [[ -z "${TO_LOG}" ]]; then + TO_LOG="${1}" + fi + fi + + echo -e "[${LOG_LEVEL}] ${TO_LOG}" +} + +ensure_mod() { + FILE="${1}" + MOD="${2}" + U_ID="${3}" + G_ID="${4}" + + chmod "${MOD}" "${FILE}" + chown "${U_ID}"."${G_ID}" "${FILE}" +} + +generate_passwd() { + hexdump -e '"%02x"' -n 16 /dev/urandom +} + +# ensure backward comaptibility for earlier versions of this image +if [[ -n "${KEYPAIR_LOGIN}" ]] && [[ "${KEYPAIR_LOGIN}" == "true" ]]; then + ROOT_KEYPAIR_LOGIN_ENABLED="${KEYPAIR_LOGIN}" +fi +if [[ -n "${ROOT_PASSWORD}" ]]; then + ROOT_LOGIN_UNLOCKED="true" +fi + +# enable root login if keypair login is enabled +if [[ "${ROOT_KEYPAIR_LOGIN_ENABLED}" == "true" ]]; then + ROOT_LOGIN_UNLOCKED="true" +fi + +# initiate default sshd-config if there is none available +if [[ ! "$(ls -A /etc/ssh)" ]]; then + cp -a "${CACHED_SSH_DIRECTORY}"/* /etc/ssh/. +fi +rm -rf "${CACHED_SSH_DIRECTORY}" + +# generate host keys if not present +ssh-keygen -A 1>/dev/null + +log "Applying configuration for 'root' user ..." + +if [[ "${ROOT_LOGIN_UNLOCKED}" == "true" ]] ; then + + # generate random root password + if [[ -z "${ROOT_PASSWORD}" ]]; then + log " generating random password for user 'root'" + ROOT_PASSWORD="$(generate_passwd)" + fi + + echo "root:${ROOT_PASSWORD}" | chpasswd &>/dev/null + log " password for user 'root' set" + log "warning" " user 'root' is now UNLOCKED" + + # set root login mode by password or keypair + if [[ "${ROOT_KEYPAIR_LOGIN_ENABLED}" == "true" ]] && [[ -f "${HOME}/.ssh/authorized_keys" ]]; then + sed -i "s/#PermitRootLogin.*/PermitRootLogin without-password/" /etc/ssh/sshd_config + sed -i "s/#PasswordAuthentication.*/PasswordAuthentication no/" /etc/ssh/sshd_config + ensure_mod "${HOME}/.ssh/authorized_keys" "0600" "root" "root" + log " enabled login by keypair and disabled password-login for user 'root'" + else + sed -i "s/#PermitRootLogin.*/PermitRootLogin\ yes/" /etc/ssh/sshd_config + log " enabled login by password for user 'root'" + fi + +else + + sed -i "s/#PermitRootLogin.*/PermitRootLogin no/" /etc/ssh/sshd_config + log " disabled login for user 'root'" + log " user 'root' is now LOCKED" + +fi + +printf "\n" + +log "Applying configuration for additional users ..." + +if [[ ! -x "${USER_LOGIN_SHELL}" ]]; then + log "error" " can not allocate desired shell '${USER_LOGIN_SHELL}', falling back to '${USER_LOGIN_SHELL_FALLBACK}' ..." + USER_LOGIN_SHELL="${USER_LOGIN_SHELL_FALLBACK}" +fi + +log " desired shell is ${USER_LOGIN_SHELL}" + + +if [[ -n "${SSH_USERS}" ]]; then + + IFS="," + for USER in ${SSH_USERS}; do + + log " '${USER}'" + + USER_NAME="$(echo "${USER}" | cut -d ':' -f 1)" + USER_UID="$(echo "${USER}" | cut -d ':' -f 2)" + USER_GID="$(echo "${USER}" | cut -d ':' -f 3)" + + if [[ -z "${USER_NAME}" ]] || [[ -z "${USER_UID}" ]] || [[ -z "${USER_GID}" ]]; then + log "error" " skipping invalid data '${USER_NAME}' - UID: '${USER_UID}' GID: '${USER_GID}'" + continue + fi + + USER_GROUP="${USER_NAME}" + if getent group "${USER_GID}" &>/dev/null ; then + USER_GROUP="$(getent group "${USER_GID}" | cut -d ':' -f 1)" + log "warning" " desired GID is already present in system. Using the present group-name - GID: '${USER_GID}' GNAME: '${USER_GROUP}'" + else + addgroup -g "${USER_GID}" "${USER_GROUP}" + fi + + if getent passwd "${USER_NAME}" &>/dev/null ; then + log "warning" " desired USER_NAME is already present in system. Skipping creation - USER_NAME: '${USER_NAME}'" + else + adduser -s "${USER_LOGIN_SHELL}" -D -u "${USER_UID}" -G "${USER_GROUP}" "${USER_NAME}" + log " user '${USER_NAME}' created - UID: '${USER_UID}' GID: '${USER_GID}' GNAME: '${USER_GROUP}'" + fi + + passwd -u "${USER_NAME}" &>/dev/null || true + mkdir -p "/home/${USER_NAME}/.ssh" + + MOUNTED_AUTHORIZED_KEYS="${AUTHORIZED_KEYS_VOLUME}/${USER_NAME}" + LOCAL_AUTHORIZED_KEYS="/home/${USER_NAME}/.ssh/authorized_keys" + + if [[ ! -e "${MOUNTED_AUTHORIZED_KEYS}" ]]; then + log "warning" " no SSH authorized_keys found for user '${USER_NAME}'" + else + cp "${MOUNTED_AUTHORIZED_KEYS}" "${LOCAL_AUTHORIZED_KEYS}" + log " copied ${MOUNTED_AUTHORIZED_KEYS} to ${LOCAL_AUTHORIZED_KEYS}" + ensure_mod "${LOCAL_AUTHORIZED_KEYS}" "0600" "${USER_NAME}" "${USER_GID}" + log " set mod 0600 on ${LOCAL_AUTHORIZED_KEYS}" + fi + + ############################################# + # ADDED + ############################################# + + log " set mod 0755 on /home/${USER_NAME}/.ssh" + ensure_mod "/home/${USER_NAME}/.ssh" "0755" "${USER_NAME}" "${USER_GID}" + + mkdir -p "/home/${USER_NAME}/.yubico" + log " set mod 0755 on /home/${USER_NAME}/.yubico" + ensure_mod "/home/${USER_NAME}/.yubico" "0755" "${USER_NAME}" "${USER_GID}" + MOUNTED_AUTHORIZED_YUBI="${AUTHORIZED_KEYS_VOLUME}/${USER_NAME}.yubi" + LOCAL_AUTHORIZED_YUBI="/home/${USER_NAME}/.yubico/authorized_yubikeys" + + if [[ -e "${MOUNTED_AUTHORIZED_YUBI}" ]]; then + cp "${MOUNTED_AUTHORIZED_YUBI}" "${LOCAL_AUTHORIZED_YUBI}" + log " copied ${MOUNTED_AUTHORIZED_YUBI} to ${LOCAL_AUTHORIZED_YUBI}" + ensure_mod "${LOCAL_AUTHORIZED_YUBI}" "0600" "${USER_NAME}" "${USER_GID}" + log " set mod 0600 on ${LOCAL_AUTHORIZED_YUBI}" + fi + + MOUNTED_CONFIG="${AUTHORIZED_KEYS_VOLUME}/${USER_NAME}.config" + LOCAL_CONFIG="/home/${USER_NAME}/.ssh/config" + + if [[ -e "${MOUNTED_CONFIG}" ]]; then + cp "${MOUNTED_CONFIG}" "${LOCAL_CONFIG}" + log " copied ${MOUNTED_CONFIG} to ${LOCAL_CONFIG}" + ensure_mod "${LOCAL_CONFIG}" "0644" "${USER_NAME}" "${USER_GID}" + log " set mod 0644 on ${LOCAL_CONFIG}" + fi + + ############################################# + # END OF ADDITION + ############################################# + + printf "\n" + + done + unset IFS + +else + + log " no additional SSH-users set" + +fi + +############################################# +# ADDED +############################################# + +sed -i "s/#ChallengeResponseAuthentication.*/ChallengeResponseAuthentication yes/" /etc/ssh/sshd_config +sed -i "s/#UsePAM.*/UsePAM yes/" /etc/ssh/sshd_config + +echo "auth sufficient pam_yubico.so id=16 debug" >> /etc/pam.d/sshd + +############################################# +# END OF ADDITION +############################################# + +echo "" + +# do not detach (-D), log to stderr (-e), passthrough other arguments +exec /usr/sbin/sshd -D -e "$@" \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..6853e29 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,22 @@ +version: "3" + +networks: + web: + external: true + +services: + ssh: + build: ./build + restart: unless-stopped + networks: + web: + environment: + SSH_USERS: + volumes: + - ./authorized_keys:/conf.d/authorized_keys + - ./ssh:/etc/ssh + labels: + traefik.enable: true + traefik.tcp.services.ssh.loadbalancer.server.port: 22 + traefik.tcp.routers.ssh.rule: HostSNI(`*`) + traefik.tcp.routers.ssh.entrypoints: websecure \ No newline at end of file