From 4936dc6b79e12e5111ba6640550ece8306d6f00f Mon Sep 17 00:00:00 2001 From: SckyzO Date: Wed, 29 Jul 2026 06:22:15 +0000 Subject: [PATCH 1/3] Add a container healthcheck The healthcheck reports the container healthy once sshd is listening on its configured port. It reads the kernel socket table instead of opening a connection, so it does not append an aborted handshake to the ssh log every 30 seconds. The port is taken from the running sshd_config, so a port set there rather than through LISTEN_PORT is honoured too. --- Dockerfile | 3 +++ Dockerfile.aarch64 | 3 +++ README.md | 1 + readme-vars.yml | 1 + root/healthcheck.sh | 21 +++++++++++++++++++++ 5 files changed, 29 insertions(+) create mode 100755 root/healthcheck.sh diff --git a/Dockerfile b/Dockerfile index 5c34b67..b508868 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,4 +38,7 @@ COPY /root / EXPOSE 2222 +HEALTHCHECK --start-period=30s --interval=30s --timeout=10s --retries=3 \ + CMD /healthcheck.sh + VOLUME /config diff --git a/Dockerfile.aarch64 b/Dockerfile.aarch64 index 6bcbac5..2ffb170 100644 --- a/Dockerfile.aarch64 +++ b/Dockerfile.aarch64 @@ -38,4 +38,7 @@ COPY /root / EXPOSE 2222 +HEALTHCHECK --start-period=30s --interval=30s --timeout=10s --retries=3 \ + CMD /healthcheck.sh + VOLUME /config diff --git a/README.md b/README.md index 830cec4..6d7546d 100644 --- a/README.md +++ b/README.md @@ -337,6 +337,7 @@ Once registered you can define the dockerfile to use with `-f Dockerfile.aarch64 ## Versions +* **03.08.26:** - Add a healthcheck reporting whether sshd is listening. * **05.07.26:** - Rebase to Alpine 3.24. * **28.12.25:** - Rebase to Alpine 3.23. * **05.07.25:** - Rebase to Alpine 3.22. diff --git a/readme-vars.yml b/readme-vars.yml index 71d9385..dd913ad 100644 --- a/readme-vars.yml +++ b/readme-vars.yml @@ -117,6 +117,7 @@ init_diagram: | "openssh-server:latest" <- Base Images # changelog changelogs: + - {date: "03.08.26:", desc: "Add a healthcheck reporting whether sshd is listening."} - {date: "05.07.26:", desc: "Rebase to Alpine 3.24."} - {date: "28.12.25:", desc: "Rebase to Alpine 3.23."} - {date: "05.07.25:", desc: "Rebase to Alpine 3.22."} diff --git a/root/healthcheck.sh b/root/healthcheck.sh new file mode 100755 index 0000000..3eab001 --- /dev/null +++ b/root/healthcheck.sh @@ -0,0 +1,21 @@ +#! /bin/bash + +# The port set in the running config wins over the env var, so a port edited +# directly in sshd_config is still picked up. +PORT=$(awk '$1 == "Port" { print $2; exit }' /config/sshd/sshd_config 2>/dev/null) +PORT=${PORT:-${LISTEN_PORT:-2222}} +if [[ ! "$PORT" =~ ^[0-9]+$ ]]; then + PORT=2222 +fi + +# The kernel socket table is read instead of opening a connection, so the check +# does not log an aborted handshake in the ssh log on every run. The local port +# is listed in hex and 0A is the listening state. +LISTENING="^[[:space:]]*[0-9]+:[[:space:]]+[0-9A-F]+:$(printf '%04X' "${PORT}")[[:space:]]+[0-9A-F]+:0+[[:space:]]+0A" + +if grep -qE "${LISTENING}" /proc/net/tcp 2>/dev/null || grep -qE "${LISTENING}" /proc/net/tcp6 2>/dev/null; then + exit 0 +fi + +echo "sshd is not listening on port ${PORT}" +exit 1 From ee1db2d9cb3cc768efe6d166cbda373fcda86ab0 Mon Sep 17 00:00:00 2001 From: SckyzO Date: Wed, 29 Jul 2026 06:23:13 +0000 Subject: [PATCH 2/3] Add optional OTP (2FA) access OTP_ACCESS=true requires a time based one time password on top of the ssh key, or on top of the password when PASSWORD_ACCESS is enabled as well. The secret lives in /config so it survives a container recreation, and until it exists the user is let in on the key alone, which is what makes the initial enrollment possible. Two details of this image shape the PAM stack. The module answers PAM_IGNORE rather than PAM_SUCCESS when nullok applies, and a stack whose only module is ignored has nothing that succeeded, so an explicit action list runs it and a trailing pam_permit carries the login; a wrong code hits default=die before reaching it. And sshd runs as the ssh user here, so it cannot read /etc/shadow, which the stock account and session checks need. Those are left out rather than inherited: without OTP this image does not enable PAM at all, so none of them run today either. The sshd_config block is delimited by markers and rebuilt on every start, so removing the variable cleans the persisted config back up. Closes linuxserver/docker-openssh-server#121 --- Dockerfile | 2 + Dockerfile.aarch64 | 2 + README.md | 15 ++++ readme-vars.yml | 13 ++++ .../s6-rc.d/init-openssh-server-config/run | 69 +++++++++++++++++++ 5 files changed, 101 insertions(+) diff --git a/Dockerfile b/Dockerfile index b508868..a1dc0dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,7 @@ LABEL maintainer="aptalca" RUN \ echo "**** install runtime packages ****" && \ apk add --no-cache --upgrade \ + google-authenticator \ logrotate \ nano \ netcat-openbsd \ @@ -28,6 +29,7 @@ RUN \ printf "Linuxserver.io version: ${VERSION}\nBuild-date: ${BUILD_DATE}" > /build_version && \ echo "**** setup openssh environment ****" && \ sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/g' /etc/ssh/sshd_config && \ + cp /etc/pam.d/sshd /etc/pam.d/sshd.default && \ usermod --shell /bin/bash abc && \ rm -rf \ /tmp/* \ diff --git a/Dockerfile.aarch64 b/Dockerfile.aarch64 index 2ffb170..1b1f78a 100644 --- a/Dockerfile.aarch64 +++ b/Dockerfile.aarch64 @@ -12,6 +12,7 @@ LABEL maintainer="aptalca" RUN \ echo "**** install runtime packages ****" && \ apk add --no-cache --upgrade \ + google-authenticator \ logrotate \ nano \ netcat-openbsd \ @@ -28,6 +29,7 @@ RUN \ printf "Linuxserver.io version: ${VERSION}\nBuild-date: ${BUILD_DATE}" > /build_version && \ echo "**** setup openssh environment ****" && \ sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/g' /etc/ssh/sshd_config && \ + cp /etc/pam.d/sshd /etc/pam.d/sshd.default && \ usermod --shell /bin/bash abc && \ rm -rf \ /tmp/* \ diff --git a/README.md b/README.md index 6d7546d..c936faa 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,17 @@ It is also possible to run multiple copies of this container with different port You can volume map your own text file to `/etc/motd` to override the message displayed upon connection. You can optionally set the docker argument `hostname` +## OTP (2FA) + +Setting `OTP_ACCESS` to `true` requires a time based one time password, on top of the ssh key, or on top of the password when `PASSWORD_ACCESS` is enabled as well. The codes are generated by any TOTP application, and the secret is kept in `/config/.google_authenticator` so it survives a container recreation. + +As long as that secret does not exist, the user is let in without being asked for a code, which is what makes the enrollment possible. Connect as usual and run: +``` +google-authenticator -t -d -f -r 3 -R 30 -W -s /config/.google_authenticator +``` + +Scan the QR code with your authenticator application and write down the scratch codes, they are the only way back in if you lose it. Every login from that point on asks for a verification code. + ## Key Generation This container has a helper script to generate an ssh private/public key. In order to generate a key please run: @@ -116,6 +127,7 @@ services: - PUBLIC_KEY_URL=https://github.com/username.keys #optional - SUDO_ACCESS=false #optional - PASSWORD_ACCESS=false #optional + - OTP_ACCESS=false #optional - USER_PASSWORD=password #optional - USER_PASSWORD_FILE=/path/to/file #optional - USER_NAME=linuxserver.io #optional @@ -142,6 +154,7 @@ docker run -d \ -e PUBLIC_KEY_URL=https://github.com/username.keys `#optional` \ -e SUDO_ACCESS=false `#optional` \ -e PASSWORD_ACCESS=false `#optional` \ + -e OTP_ACCESS=false `#optional` \ -e USER_PASSWORD=password `#optional` \ -e USER_PASSWORD_FILE=/path/to/file `#optional` \ -e USER_NAME=linuxserver.io `#optional` \ @@ -169,6 +182,7 @@ Containers are configured using parameters passed at runtime (such as those abov | `-e PUBLIC_KEY_URL=https://github.com/username.keys` | Optionally specify a URL containing the public key. | | `-e SUDO_ACCESS=false` | Set to `true` to allow `linuxserver.io`, the ssh user, sudo access. Without `USER_PASSWORD` set, this will allow passwordless sudo access. | | `-e PASSWORD_ACCESS=false` | Set to `true` to allow user/password ssh access. You will want to set `USER_PASSWORD` or `USER_PASSWORD_FILE` as well. | +| `-e OTP_ACCESS=false` | Set to `true` to require a one time password (2FA) in addition to the ssh key or the password. See the OTP section below. | | `-e USER_PASSWORD=password` | Optionally set a sudo password for `linuxserver.io`, the ssh user. If this or `USER_PASSWORD_FILE` are not set but `SUDO_ACCESS` is set to true, the user will have passwordless sudo access. | | `-e USER_PASSWORD_FILE=/path/to/file` | Optionally specify a file that contains the password. This setting supersedes the `USER_PASSWORD` option (works with docker secrets). | | `-e USER_NAME=linuxserver.io` | Optionally specify a user name (Default:`linuxserver.io`) | @@ -337,6 +351,7 @@ Once registered you can define the dockerfile to use with `-f Dockerfile.aarch64 ## Versions +* **03.08.26:** - Add optional OTP (2FA) support via `OTP_ACCESS`. * **03.08.26:** - Add a healthcheck reporting whether sshd is listening. * **05.07.26:** - Rebase to Alpine 3.24. * **28.12.25:** - Rebase to Alpine 3.23. diff --git a/readme-vars.yml b/readme-vars.yml index dd913ad..345b55f 100644 --- a/readme-vars.yml +++ b/readme-vars.yml @@ -32,6 +32,7 @@ opt_param_env_vars: - {env_var: "PUBLIC_KEY_URL", env_value: "https://github.com/username.keys", desc: "Optionally specify a URL containing the public key."} - {env_var: "SUDO_ACCESS", env_value: "false", desc: "Set to `true` to allow `linuxserver.io`, the ssh user, sudo access. Without `USER_PASSWORD` set, this will allow passwordless sudo access."} - {env_var: "PASSWORD_ACCESS", env_value: "false", desc: "Set to `true` to allow user/password ssh access. You will want to set `USER_PASSWORD` or `USER_PASSWORD_FILE` as well."} + - {env_var: "OTP_ACCESS", env_value: "false", desc: "Set to `true` to require a one time password (2FA) in addition to the ssh key or the password. See the OTP section below."} - {env_var: "USER_PASSWORD", env_value: "password", desc: "Optionally set a sudo password for `linuxserver.io`, the ssh user. If this or `USER_PASSWORD_FILE` are not set but `SUDO_ACCESS` is set to true, the user will have passwordless sudo access."} - {env_var: "USER_PASSWORD_FILE", env_value: "/path/to/file", desc: "Optionally specify a file that contains the password. This setting supersedes the `USER_PASSWORD` option (works with docker secrets)."} - {env_var: "USER_NAME", env_value: "linuxserver.io", desc: "Optionally specify a user name (Default:`linuxserver.io`)"} @@ -63,6 +64,17 @@ app_setup_block: | You can volume map your own text file to `/etc/motd` to override the message displayed upon connection. You can optionally set the docker argument `hostname` + ## OTP (2FA) + + Setting `OTP_ACCESS` to `true` requires a time based one time password, on top of the ssh key, or on top of the password when `PASSWORD_ACCESS` is enabled as well. The codes are generated by any TOTP application, and the secret is kept in `/config/.google_authenticator` so it survives a container recreation. + + As long as that secret does not exist, the user is let in without being asked for a code, which is what makes the enrollment possible. Connect as usual and run: + ``` + google-authenticator -t -d -f -r 3 -R 30 -W -s /config/.google_authenticator + ``` + + Scan the QR code with your authenticator application and write down the scratch codes, they are the only way back in if you lose it. Every login from that point on asks for a verification code. + ## Key Generation This container has a helper script to generate an ssh private/public key. In order to generate a key please run: @@ -117,6 +129,7 @@ init_diagram: | "openssh-server:latest" <- Base Images # changelog changelogs: + - {date: "03.08.26:", desc: "Add optional OTP (2FA) support via `OTP_ACCESS`."} - {date: "03.08.26:", desc: "Add a healthcheck reporting whether sshd is listening."} - {date: "05.07.26:", desc: "Rebase to Alpine 3.24."} - {date: "28.12.25:", desc: "Rebase to Alpine 3.23."} diff --git a/root/etc/s6-overlay/s6-rc.d/init-openssh-server-config/run b/root/etc/s6-overlay/s6-rc.d/init-openssh-server-config/run index 1f24239..e518a79 100755 --- a/root/etc/s6-overlay/s6-rc.d/init-openssh-server-config/run +++ b/root/etc/s6-overlay/s6-rc.d/init-openssh-server-config/run @@ -86,6 +86,75 @@ else echo "User/password ssh access is disabled." fi +# otp (2FA) access +OTP_SECRET=/config/.google_authenticator + +# rebuilt from a known state on every start so the variable can be turned back +# off again. sshd resolves the plain sshd PAM service, not sshd.pam. +if [[ "$OTP_ACCESS" == "true" ]] || grep -q "pam_google_authenticator.so" /etc/pam.d/sshd; then + cp /etc/pam.d/sshd.default /etc/pam.d/sshd + + if [[ "$OTP_ACCESS" == "true" ]]; then + { + echo "#%PAM-1.0" + + # the whole auth stack runs for keyboard-interactive, so the unix + # password prompt is only carried over when password access is on + if [[ "$PASSWORD_ACCESS" == "true" ]]; then + grep -E '^[[:space:]]*auth[[:space:]]' /etc/pam.d/sshd.default + fi + + # nullok lets a user that has not enrolled yet log in, which is what + # makes the first otp-setup run possible. The module says so by + # answering PAM_IGNORE, and a stack whose only module is ignored has + # nothing that succeeded, so it is the trailing pam_permit that + # carries the login. A wrong code dies before reaching it. + echo "auth [success=ok ignore=ignore default=die] pam_google_authenticator.so nullok secret=${OTP_SECRET}" + echo "auth required pam_permit.so" + + # the stock account and session checks read /etc/shadow, which sshd + # cannot do here because it runs as the ssh user rather than root. + # Nothing is lost by leaving them out: without OTP this image does + # not enable PAM at all, so none of them run today either. + echo "account required pam_permit.so" + echo "password required pam_permit.so" + echo "session required pam_permit.so" + } > /etc/pam.d/sshd + fi +fi +sed -i '/^# >>> OTP_ACCESS >>>$/,/^# <<< OTP_ACCESS <<<$/d' /config/sshd/sshd_config + +if [[ "$OTP_ACCESS" == "true" ]]; then + if [[ "$PASSWORD_ACCESS" == "true" ]]; then + AUTH_METHODS="keyboard-interactive publickey,keyboard-interactive" + else + AUTH_METHODS="publickey,keyboard-interactive" + fi + + # sshd keeps the first value it reads for a keyword, so an active setting + # earlier in the file silently wins over the block appended below + if grep -qE '^[[:space:]]*(UsePAM|KbdInteractiveAuthentication|AuthenticationMethods)[[:space:]]' /config/sshd/sshd_config; then + echo "*** WARNING: /config/sshd/sshd_config already sets UsePAM, KbdInteractiveAuthentication or AuthenticationMethods. Remove those lines, otherwise OTP will not be enforced. ***" + fi + + cat >> /config/sshd/sshd_config << EOF +# >>> OTP_ACCESS >>> +UsePAM yes +KbdInteractiveAuthentication yes +AuthenticationMethods ${AUTH_METHODS} +# <<< OTP_ACCESS <<< +EOF + + if [[ -f "$OTP_SECRET" ]]; then + echo "OTP (2FA) ssh access is enabled." + else + echo "OTP (2FA) ssh access is enabled, but ${USER_NAME} has no secret yet and is not prompted for a code." + echo "Run 'google-authenticator -t -d -f -r 3 -R 30 -W -s ${OTP_SECRET}' as ${USER_NAME} to enroll." + fi +else + echo "OTP (2FA) ssh access is disabled." +fi + # set umask for sftp UMASK=${UMASK:-022} sed -i "s|/usr/lib/ssh/sftp-server$|/usr/lib/ssh/sftp-server -u ${UMASK}|g" /config/sshd/sshd_config From ae1a1d59e84b30a3e6ec36b668a207f0e43ce4b2 Mon Sep 17 00:00:00 2001 From: SckyzO Date: Wed, 29 Jul 2026 06:23:37 +0000 Subject: [PATCH 3/3] Add an otp-setup helper and an enrollment banner Enrolling meant typing a google-authenticator command line with the right secret path and answering six prompts. otp-setup wraps that: it writes to the same file the PAM module reads, backs up an existing secret before replacing it, and --show prints the secret and scratch codes again. It also drops the confirmation step when there is no terminal, so it works under docker exec instead of failing on a prompt it cannot read. Because the module lets an unenrolled user straight in, nothing told them the secret was still missing. A profile.d snippet now says so on login, and stops once the secret exists. --- README.md | 10 ++- readme-vars.yml | 10 ++- root/etc/profile.d/otp-setup.sh | 22 ++++++ .../s6-rc.d/init-openssh-server-config/run | 2 +- root/usr/local/bin/otp-setup | 74 +++++++++++++++++++ 5 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 root/etc/profile.d/otp-setup.sh create mode 100755 root/usr/local/bin/otp-setup diff --git a/README.md b/README.md index c936faa..677a8c9 100644 --- a/README.md +++ b/README.md @@ -82,14 +82,16 @@ You can optionally set the docker argument `hostname` ## OTP (2FA) -Setting `OTP_ACCESS` to `true` requires a time based one time password, on top of the ssh key, or on top of the password when `PASSWORD_ACCESS` is enabled as well. The codes are generated by any TOTP application, and the secret is kept in `/config/.google_authenticator` so it survives a container recreation. +Set `OTP_ACCESS` to `true` to require a time based one time password on top of the ssh key, or on top of the password when `PASSWORD_ACCESS` is enabled as well. Any TOTP application can generate the codes. The secret is stored in `/config/.google_authenticator`, so it survives recreating the container. -As long as that secret does not exist, the user is let in without being asked for a code, which is what makes the enrollment possible. Connect as usual and run: +Until that secret exists, the key alone still gets you in. That is what makes the first login possible, and a banner points you at the command to run: ``` -google-authenticator -t -d -f -r 3 -R 30 -W -s /config/.google_authenticator +otp-setup ``` -Scan the QR code with your authenticator application and write down the scratch codes, they are the only way back in if you lose it. Every login from that point on asks for a verification code. +Scan the QR code with your authenticator application, then write down the scratch codes. They are the only way back in if you lose the application. From the next login on, sshd asks for a verification code. + +`otp-setup --show` prints the secret again. Running `otp-setup` a second time replaces it, after asking for confirmation. ## Key Generation diff --git a/readme-vars.yml b/readme-vars.yml index 345b55f..1ec13da 100644 --- a/readme-vars.yml +++ b/readme-vars.yml @@ -66,14 +66,16 @@ app_setup_block: | ## OTP (2FA) - Setting `OTP_ACCESS` to `true` requires a time based one time password, on top of the ssh key, or on top of the password when `PASSWORD_ACCESS` is enabled as well. The codes are generated by any TOTP application, and the secret is kept in `/config/.google_authenticator` so it survives a container recreation. + Set `OTP_ACCESS` to `true` to require a time based one time password on top of the ssh key, or on top of the password when `PASSWORD_ACCESS` is enabled as well. Any TOTP application can generate the codes. The secret is stored in `/config/.google_authenticator`, so it survives recreating the container. - As long as that secret does not exist, the user is let in without being asked for a code, which is what makes the enrollment possible. Connect as usual and run: + Until that secret exists, the key alone still gets you in. That is what makes the first login possible, and a banner points you at the command to run: ``` - google-authenticator -t -d -f -r 3 -R 30 -W -s /config/.google_authenticator + otp-setup ``` - Scan the QR code with your authenticator application and write down the scratch codes, they are the only way back in if you lose it. Every login from that point on asks for a verification code. + Scan the QR code with your authenticator application, then write down the scratch codes. They are the only way back in if you lose the application. From the next login on, sshd asks for a verification code. + + `otp-setup --show` prints the secret again. Running `otp-setup` a second time replaces it, after asking for confirmation. ## Key Generation diff --git a/root/etc/profile.d/otp-setup.sh b/root/etc/profile.d/otp-setup.sh new file mode 100644 index 0000000..8861f0a --- /dev/null +++ b/root/etc/profile.d/otp-setup.sh @@ -0,0 +1,22 @@ +# Point a user that OTP applies to, but who has not enrolled yet, at otp-setup. +# Until they do, pam_google_authenticator lets them in without asking for a +# code, so nothing else would tell them the secret is still missing. + +[ -z "$SSH_CONNECTION" ] && return 0 +grep -q "pam_google_authenticator.so" /etc/pam.d/sshd 2>/dev/null || return 0 +[ -f /config/.google_authenticator ] && return 0 + +cat << 'BANNER' + +──────────────────────────────────────────────────────────── + Two factor authentication is enabled on this server, but + you have not set up your authenticator application yet. + + Run: + otp-setup + + to get your QR code and your scratch codes. Your next + login then asks for a 6 digit code on top of your key. +──────────────────────────────────────────────────────────── + +BANNER diff --git a/root/etc/s6-overlay/s6-rc.d/init-openssh-server-config/run b/root/etc/s6-overlay/s6-rc.d/init-openssh-server-config/run index e518a79..d97e37e 100755 --- a/root/etc/s6-overlay/s6-rc.d/init-openssh-server-config/run +++ b/root/etc/s6-overlay/s6-rc.d/init-openssh-server-config/run @@ -149,7 +149,7 @@ EOF echo "OTP (2FA) ssh access is enabled." else echo "OTP (2FA) ssh access is enabled, but ${USER_NAME} has no secret yet and is not prompted for a code." - echo "Run 'google-authenticator -t -d -f -r 3 -R 30 -W -s ${OTP_SECRET}' as ${USER_NAME} to enroll." + echo "Log in and run 'otp-setup' to enroll." fi else echo "OTP (2FA) ssh access is disabled." diff --git a/root/usr/local/bin/otp-setup b/root/usr/local/bin/otp-setup new file mode 100755 index 0000000..f822437 --- /dev/null +++ b/root/usr/local/bin/otp-setup @@ -0,0 +1,74 @@ +#! /bin/bash +# Generate, regenerate or display the TOTP secret used by OTP_ACCESS. + +set -e + +# absolute, and matching the secret= passed to pam_google_authenticator, so the +# helper and the PAM module always agree on the file regardless of $HOME +SECRET_FILE=/config/.google_authenticator + +usage() { + cat << EOF +Usage: + otp-setup Generate a TOTP secret, or regenerate it after asking. + Prints the QR code, the secret and the scratch codes. + otp-setup --show Print the current secret file again. + otp-setup --help Show this help. +EOF +} + +case "${1:-}" in + --show|-s) + if [[ ! -f "$SECRET_FILE" ]]; then + echo "No OTP secret yet. Run 'otp-setup' to create one." + exit 1 + fi + echo "This prints your secret and scratch codes in clear text. Make sure" + echo "nobody is watching and that this session is not being recorded." + echo + echo "=== ${SECRET_FILE} ===" + echo "First line is the base32 secret, the trailing digit lines are the" + echo "emergency scratch codes." + echo + cat "$SECRET_FILE" + ;; + --help|-h) + usage + ;; + "") + if [[ -f "$SECRET_FILE" ]]; then + read -r -p "A secret already exists in ${SECRET_FILE}. Regenerating invalidates the old one. Continue? [y/N] " answer || answer="" + if [[ ! "$answer" =~ ^[Yy]$ ]]; then + echo "Aborted." + exit 0 + fi + backup="${SECRET_FILE}.bak.$(date +%s)" + cp "$SECRET_FILE" "$backup" + echo "Old secret kept in ${backup}" + fi + + # typing the first code back catches a QR that was scanned wrong, but + # there is nothing to type it with when this runs without a terminal, + # as it does under docker exec + confirm=() + if [[ ! -t 0 ]]; then + confirm=(--no-confirm) + fi + + # -t time based, -d one use per code, -f write without asking, + # -r 3 -R 30 three attempts per 30s, -W narrow the accepted time window + google-authenticator -t -d -f -r 3 -R 30 -W "${confirm[@]}" -s "$SECRET_FILE" + chmod 600 "$SECRET_FILE" + + echo + echo "=== setup complete ===" + echo "Scan the QR code above with your authenticator application." + echo "Write down the scratch codes too. They are the only way back in if" + echo "you lose the application." + echo "Your next login asks for a 6 digit code." + ;; + *) + usage + exit 2 + ;; +esac