@@ -461,9 +461,11 @@ phased rollout that subsumes Options A and B.
461461The ` rsa ` crate (already a transitive dependency via
462462` jsonwebtoken ` 's ` rust_crypto ` feature) must be added as a
463463** direct** dependency in ` Cargo.toml ` along with ` rand ` (for
464- ` OsRng ` ). PEM export requires the ` pkcs8 ` + ` pem ` features on
465- ` rsa ` (for ` EncodePrivateKey::to_pkcs8_pem ` ) and ` spki ` (for
466- ` EncodePublicKey::to_public_key_pem ` ).
464+ ` OsRng ` ). PEM export uses ` EncodePrivateKey::to_pkcs8_pem `
465+ (from ` pkcs8 ` ) and ` EncodePublicKey::to_public_key_pem ` (from
466+ ` spki ` ). Both are pulled in transitively by the ` pem ` feature
467+ on ` rsa ` — no extra feature flags are needed beyond
468+ ` rsa = { features = ["std", "pem"] } ` .
467469
468470##### Key generation details
469471
@@ -527,7 +529,7 @@ touches the following files (non-exhaustive):
527529| ` compose.yaml ` | Remove or comment out ` AUTH__PRIVATE_KEY_PATH ` / ` AUTH__PUBLIC_KEY_PATH ` env vars |
528530| ` src/web/api/server/v1/contexts/user/mod.rs ` | Update module-level doc example |
529531
530- #### Phase 6 — ` generate-auth-keypair ` CLI + Container Auto-Generation
532+ #### Phase 6 — ` generate-auth-keypair ` CLI + Container Auto-Generation ✅ Implemented
531533
532534##### Motivation
533535
@@ -560,43 +562,87 @@ pair and writes both PEM blocks to **stdout**. Design constraints:
560562 standard PEM (Base64-encoded PKCS #8 / SPKI) format. The two
561563 blocks are self-delimiting via their ` -----BEGIN …----- ` /
562564 ` -----END …----- ` markers.
563- - ** Diagnostic message on stderr** confirming the key was
564- generated (type, bit size).
565- - Uses ` clap ` (already a dependency) for ` --help ` and future
566- extensibility (e.g., ` --bits ` , ` --out-dir ` ).
565+ - ** Diagnostic output on stderr** via ` tracing ` (already a
566+ dependency). A ` --debug ` flag switches the subscriber from
567+ the default ` info ` level to ` debug ` , giving deployers
568+ detailed timing and key-fingerprint output without polluting
569+ the PEM stream on stdout.
570+ - Uses ` clap ` (already a dependency) for polished ` --help `
571+ output and future extensibility (e.g., ` --bits ` , ` --out-dir ` ).
567572- Reuses the same ` rsa ` + ` pkcs8 ` code path as the ephemeral
568573 generator in ` src/jwt.rs ` .
569574
570575##### Container integration
571576
572577The container entry script (` share/container/entry_script_sh ` )
573- auto-generates persistent keys on first boot:
578+ auto-generates persistent keys on first boot (runs ** after**
579+ ` adduser ` , so the ` torrust ` user already exists):
574580
575581``` sh
576582# Generate auth keys if not already present on the volume.
577- private_key=" /etc/torrust/index/private.pem"
578- public_key=" /etc/torrust/index/public.pem"
579-
580- if [ ! -f " $private_key " ] || [ ! -f " $public_key " ]; then
581- torrust-generate-auth-keypair > /tmp/auth_keys.pem 2> /dev/null
582- sed -n ' /BEGIN PRIVATE/,/END PRIVATE/p' /tmp/auth_keys.pem > " $private_key "
583- sed -n ' /BEGIN PUBLIC/,/END PUBLIC/p' /tmp/auth_keys.pem > " $public_key "
584- rm -f /tmp/auth_keys.pem
583+ auth_dir=" /etc/torrust/index/auth"
584+ private_key=" $auth_dir /private.pem"
585+ public_key=" $auth_dir /public.pem"
586+ tmpfile=$( mktemp /tmp/auth_keys.XXXXXX)
587+ chmod 0600 " $tmpfile "
588+ trap ' rm -f "$tmpfile"' EXIT
589+
590+ if [ ! -s " $private_key " ] || [ ! -s " $public_key " ]; then
591+ mkdir -p " $auth_dir "
592+ chown torrust:torrust " $auth_dir "
593+ chmod 0700 " $auth_dir "
594+
595+ if ! torrust-generate-auth-keypair > " $tmpfile " ; then
596+ echo " ERROR: Failed to generate auth keypair" >&2
597+ exit 1
598+ fi
599+ sed -n ' /BEGIN PRIVATE KEY/,/END PRIVATE KEY/p' " $tmpfile " > " $private_key "
600+ sed -n ' /BEGIN PUBLIC KEY/,/END PUBLIC KEY/p' " $tmpfile " > " $public_key "
601+ rm -f " $tmpfile "
585602 chown torrust:torrust " $private_key " " $public_key "
586603 chmod 0400 " $private_key "
587604 chmod 0440 " $public_key "
588605fi
589606```
590607
608+ Hardening notes:
609+ - ** ` mkdir -p "$auth_dir" ` with ` chmod 0700 ` ** creates the
610+ ` auth/ ` subdirectory with restrictive permissions before any
611+ key material is written.
612+ - ** ` mktemp ` + ` chmod 0600 ` ** creates the temp file with a
613+ unique name and restrictive permissions immediately, so key
614+ material is never world-readable — even momentarily.
615+ - ** ` [ ! -s … ] ` ** (not ` [ ! -f … ] ` ) checks that the file
616+ exists ** and** is non-empty, protecting against a previous run
617+ that was killed mid-write and left a 0-byte PEM file.
618+ - ** ` trap … EXIT ` ** ensures the temp file is cleaned up even if
619+ the script is interrupted between write and ` rm ` . ` /tmp ` in
620+ the container is typically ` tmpfs ` (RAM-backed), so the key
621+ material never touches persistent storage.
622+ - ** ` sed ` patterns match the exact PEM markers** (`BEGIN PRIVATE
623+ KEY` / ` BEGIN PUBLIC KEY`) produced by PKCS #8 / SPKI encoding,
624+ rather than loose substrings.
625+ - ** stderr flows to the container log** — only stdout is
626+ redirected to the temp file, so diagnostic ` tracing ` output
627+ and any error messages from the binary are visible in
628+ ` docker logs ` . If generation fails, the script exits
629+ non-zero.
630+ - ** TOCTOU note:** if two containers race against the same
631+ volume, both could pass the ` [ ! -s … ] ` check and
632+ overwrite each other's keys. This is unlikely in practice
633+ (single-container deployments are the norm), but can be
634+ mitigated with ` flock ` if needed.
635+
591636Because ` /etc/torrust/index ` is a declared ` VOLUME ` , the
592637generated keys persist across container restarts and image
593638upgrades. Sessions survive as long as the volume is retained.
594639
595- All container configuration files (` share/default/config/ ` ) set:
640+ All ** container** configuration files (those with ` container ` in
641+ the name under ` share/default/config/ ` ) set:
596642``` toml
597643[auth ]
598- private_key_path = " /etc/torrust/index/private.pem"
599- public_key_path = " /etc/torrust/index/public.pem"
644+ private_key_path = " /etc/torrust/index/auth/ private.pem"
645+ public_key_path = " /etc/torrust/index/auth/ public.pem"
600646```
601647
602648##### Containerfile changes
@@ -606,25 +652,30 @@ both the debug and release runtime images alongside
606652` torrust-index ` and ` health_check ` :
607653
608654``` dockerfile
609- # Extract and Test (debug)
655+ # Extract and Test (debug) — add to the existing cp -l line:
610656RUN mkdir -p /app/bin/; \
611657 cp -l /test/src/target/debug/torrust-index /app/bin/torrust-index; \
612658 cp -l /test/src/target/debug/torrust-generate-auth-keypair /app/bin/torrust-generate-auth-keypair
613659
614- # Extract and Test (release)
660+ # Extract and Test (release) — add to the existing cp -l block:
615661RUN mkdir -p /app/bin/; \
616662 cp -l /test/src/target/release/torrust-index /app/bin/torrust-index; \
617663 cp -l /test/src/target/release/health_check /app/bin/health_check; \
618664 cp -l /test/src/target/release/torrust-generate-auth-keypair /app/bin/torrust-generate-auth-keypair
619665```
620666
667+ Note: the debug stage currently copies only ` torrust-index `
668+ (no ` health_check ` ). The new binary follows the same pattern.
669+ The release stage already copies both ` torrust-index ` and
670+ ` health_check ` , so the new binary is appended to that block.
671+
621672##### Host-supplied keys (custom key workflow)
622673
623674Hosts who want to use their own RSA key pair have two options:
624675
6256761 . ** Pre-supply before first boot.** Mount or copy key files into
626677 the ` /etc/torrust/index ` volume before starting the container.
627- The entry script's existence check (` [ ! -f … ] ` ) will skip
678+ The entry script's existence check (` [ ! -s … ] ` ) will skip
628679 generation and the server will use the host's keys directly.
629680
6306812 . ** Overwrite after first boot.** Let the container auto-generate
@@ -637,19 +688,26 @@ Hosts who want to use their own RSA key pair have two options:
637688##### Usage outside containers
638689
639690``` sh
640- # Generate and split into two files:
641- cargo run --bin torrust-generate-auth-keypair \
642- | tee >( sed -n ' /BEGIN PRIVATE/,/END PRIVATE/p' > private.pem) \
643- >( sed -n ' /BEGIN PUBLIC/,/END PUBLIC/p' > public.pem) \
644- > /dev/null
691+ tmpfile=$( mktemp /tmp/auth_keys.XXXXXX)
692+ chmod 0600 " $tmpfile "
693+ cargo run --bin torrust-generate-auth-keypair > " $tmpfile "
694+ sed -n ' /BEGIN PRIVATE KEY/,/END PRIVATE KEY/p' " $tmpfile " > private.pem
695+ sed -n ' /BEGIN PUBLIC KEY/,/END PUBLIC KEY/p' " $tmpfile " > public.pem
696+ rm -f " $tmpfile "
645697```
646698
699+ > ** Avoid** the Bash process-substitution form
700+ > (` tee >(sed …) >(sed …) ` ). The ` >(…) ` sub-processes run
701+ > asynchronously, so the ` sed ` writes may not have flushed
702+ > when the pipeline exits — producing truncated PEM files.
703+ > The POSIX version above is strictly correct.
704+
647705##### Files affected
648706
649707| File | Change |
650708| ---| ---|
651- | ` src/bin/torrust-generate-auth-keypair .rs ` | New binary |
652- | ` Cargo.toml ` | No change — auto-discovered by Cargo |
709+ | ` src/bin/generate_auth_keypair .rs ` | New binary |
710+ | ` Cargo.toml ` | Add ` [[bin]] ` section: ` name = "torrust-generate-auth-keypair" ` , ` path = "src/bin/generate_auth_keypair.rs" ` |
653711| ` Containerfile ` | Copy ` torrust-generate-auth-keypair ` into ` /app/bin/ ` in both debug and release stages |
654712| ` share/container/entry_script_sh ` | Add key-generation block before ` exec su-exec ` |
655713| ` share/default/config/index.container.sqlite3.toml ` | Add ` private_key_path ` / ` public_key_path ` to ` [auth] ` |
@@ -658,6 +716,10 @@ cargo run --bin torrust-generate-auth-keypair \
658716| ` share/default/config/index.public.e2e.container.mysql.toml ` | Add ` private_key_path ` / ` public_key_path ` to ` [auth] ` |
659717| ` share/default/config/index.private.e2e.container.sqlite3.toml ` | Add ` private_key_path ` / ` public_key_path ` to ` [auth] ` |
660718
719+ Note: there is no ` index.private.e2e.container.mysql.toml ` at
720+ present. If one is added in the future, it will also need the
721+ ` [auth] ` key paths.
722+
661723##### No breaking changes
662724
663725This phase adds a new binary and updates the container entry
@@ -694,7 +756,7 @@ long as the volume is retained.
694756Note: the ** serialized default config** changes in Phase 5 — the
695757bare-metal ` [auth] ` section will no longer contain
696758` private_key_path ` / ` public_key_path ` entries. Container configs
697- * do* include these paths (pointing to ` /etc/torrust/index/ ` ).
759+ * do* include these paths (pointing to ` /etc/torrust/index/auth/ ` ).
698760Deployers who generate their config from defaults should be aware
699761of this difference. Existing configs that explicitly set these
700762fields are unaffected.
0 commit comments