Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ d8 user lock test-user 10m --timeout 5m
d8 user unlock test-user --timeout 5m

# Reset password (bcrypt hash is required)
HASH="$(echo -n 'Test12345!' | htpasswd -BinC 10 \"\" | cut -d: -f2 | tr -d '\n')"
# d8 tools htpasswd is a drop-in analog of Apache htpasswd, so no external tool is needed
HASH="$(echo -n 'Test12345!' | d8 tools htpasswd -BinC 10 \"\" | cut -d: -f2 | tr -d '\n')"
d8 user reset-password test-user "$HASH" --timeout 5m
```

Expand Down
11 changes: 10 additions & 1 deletion cmd/d8/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,15 @@ func execute() {
fmt.Fprintf(os.Stderr, "Error executing command: %v\n", err)
}

os.Exit(1)
// Commands may attach an htpasswd-style exit code via an ExitCode()
// method (see internal/tools/htpasswd); everything else exits 1.
exitCode := 1

var coder interface{ ExitCode() int }
if errors.As(err, &coder) {
exitCode = coder.ExitCode()
}

os.Exit(exitCode)
}
}
246 changes: 246 additions & 0 deletions docs/mirror-bundle-layout.md

Large diffs are not rendered by default.

70 changes: 70 additions & 0 deletions internal/tools/htpasswd/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# htpasswd

A self-contained, pure-Go analog of Apache `htpasswd`. It manages password files and hashes passwords without requiring the external `htpasswd` binary (from `apache2-utils` / `httpd-tools`). It is a drop-in for the common `htpasswd` invocations and interoperates with real htpasswd, nginx, Apache, and Dex in both directions.

## Usage

```
d8 tools htpasswd [-cbdps... -C cost -r rounds] passwordfile username
d8 tools htpasswd -b [...] passwordfile username password
d8 tools htpasswd -n [-bmBdps...] [username]
d8 tools htpasswd -D passwordfile username
d8 tools htpasswd -v passwordfile username
```

## Flags

Every Apache htpasswd flag is supported, including flag bundling (`-nbB`). Three flags are **d8 extensions** with no Apache htpasswd equivalent: the SHA-crypt algorithm flags `-2` (SHA-256) and `-5` (SHA-512), and `-r` (rounds).

| Flag | Meaning |
|------|---------|
| `-c` | Create a new password file, overwriting any existing one. |
| `-n` | Do not update a file; print the result to stdout. |
| `-D` | Delete the given user from the password file. |
| `-v` | Verify the given password for the user. |
| `-b` | Batch mode: take the password from the command line. |
| `-i` | Read the password from stdin without confirmation. |
| `-C` | bcrypt cost/work factor (4–31); only with `-B`. |
| `-r` | SHA-256/512 rounds (1000–999999999); only with `-2`/`-5`. **d8 extension.** |

## Algorithms

Select one; the default is bcrypt.

| Flag | Scheme | Notes |
|------|--------|-------|
| `-B` | bcrypt (`$2y$`) | Secure. The default. |
| `-m` | Apache MD5 / apr1 (`$apr1$`) | Legacy htpasswd default. |
| `-2` | SHA-256 crypt (`$5$`) | Secure. **d8 extension** (not in Apache htpasswd). |
| `-5` | SHA-512 crypt (`$6$`) | Secure. **d8 extension** (not in Apache htpasswd). |
| `-d` | CRYPT / DES | **Insecure**: only the first 8 characters are used. |
| `-s` | SHA-1 (`{SHA}`) | **Insecure**: unsalted. |
| `-p` | plaintext | **Insecure**: no hashing. |

## Differences from Apache htpasswd

Apache htpasswd defaults to apr1-MD5 (and, for `-B`, to bcrypt cost 5). `d8 tools htpasswd` defaults to **bcrypt at cost 10** so the output is strong and directly usable by `d8 iam user create` / `d8 iam user reset-password`. d8 also adds extensions Apache htpasswd lacks: with `-n` and no username it prints the bare hash (Apache htpasswd always requires a username and prints `username:hash`), which is exactly what `--password-hash` expects; and the SHA-crypt algorithms `-2`/`-5` plus the `-r` rounds flag (Apache htpasswd has no `-2`, `-5`, or `-r`). Each algorithm flag Apache htpasswd also defines (`-B`, `-m`, `-d`, `-s`, `-p`) behaves identically, and bcrypt output uses the `$2y$` identifier for byte-level parity. One further divergence: d8 allows bcrypt `-C` up to 31 (Apache caps it at 17). Otherwise d8 mirrors Apache htpasswd's exit codes — 2 (usage/syntax), 3 (verification failure), 5 (over-long username), 6 (bad or missing user), and 1 (file-access errors).

Parity is verified against Apache httpd `htpasswd` 2.4.58 (`apache2-utils`); the crypt-family hash outputs are additionally cross-checked byte-for-byte against OpenSSL 3.0.13, libxcrypt 4.4.36, and Python 3.12.3 `crypt`.

## Examples

```bash
# Create a file and add a user (prompts for the password)
d8 tools htpasswd -c users.htpasswd alice

# Add/update a user non-interactively (password from stdin)
echo -n 'S3cret!' | d8 tools htpasswd -i users.htpasswd bob

# Verify a password, then delete the user
d8 tools htpasswd -bv users.htpasswd bob 'S3cret!'
d8 tools htpasswd -D users.htpasswd bob

# Print a bcrypt hash for 'd8 iam user ... --password-hash'
HASH="$(echo -n 'Test12345!' | d8 tools htpasswd -ni)"
d8 iam user reset-password test-user --password-hash "$HASH"

# apr1 line for an Ingress basic-auth secret; SHA-512 crypt with custom rounds
d8 tools htpasswd -nbm admin 'S3cret!'
d8 tools htpasswd -nb5 -r 100000 admin 'S3cret!'
```
52 changes: 52 additions & 0 deletions internal/tools/htpasswd/cmd/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
Copyright 2026 Flant JSC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package cmd

import (
"github.com/spf13/pflag"

"github.com/deckhouse/deckhouse-cli/internal/tools/htpasswd"
)

// addFlags wires up the Apache htpasswd flag surface. Long names are added for
// readability; the single-character shorthands match htpasswd exactly so
// existing muscle memory and scripts keep working (including bundling like
// -nbB and digit algorithm flags -2 / -5).
func addFlags(flags *pflag.FlagSet) {
// Operation mode.
flags.BoolP(htpasswd.FlagCreate, "c", false, "Create a new password file, overwriting any existing one.")
flags.BoolP(htpasswd.FlagStdout, "n", false, "Do not update a file; print the result to stdout.")
flags.BoolP(htpasswd.FlagDelete, "D", false, "Delete the given user from the password file.")
flags.BoolP(htpasswd.FlagVerify, "v", false, "Verify the given password for the user.")

// Password source.
flags.BoolP(htpasswd.FlagBatch, "b", false, "Batch mode: take the password from the command line.")
flags.BoolP(htpasswd.FlagStdin, "i", false, "Read the password from stdin without verification.")

// Algorithm selection (default: bcrypt).
flags.BoolP(htpasswd.FlagBcrypt, "B", false, "Use bcrypt (secure; the default).")
flags.BoolP(htpasswd.FlagMD5, "m", false, "Use Apache MD5 (apr1).")
flags.BoolP(htpasswd.FlagSHA256, "2", false, "Use SHA-256 crypt (secure).")
flags.BoolP(htpasswd.FlagSHA512, "5", false, "Use SHA-512 crypt (secure).")
flags.BoolP(htpasswd.FlagCrypt, "d", false, "Use CRYPT (DES; INSECURE, 8-char limit).")
flags.BoolP(htpasswd.FlagSHA1, "s", false, "Use SHA-1 (INSECURE, unsalted).")
flags.BoolP(htpasswd.FlagPlaintext, "p", false, "Store the password in plaintext (INSECURE).")

// Algorithm parameters.
flags.IntP(htpasswd.FlagCost, "C", htpasswd.DefaultBcryptCost, "bcrypt cost/work factor (4-31); only with -B.")
flags.IntP(htpasswd.FlagRounds, "r", 0, "SHA-256/512 rounds (1000-999999999); only with -2/-5.")
}
87 changes: 87 additions & 0 deletions internal/tools/htpasswd/cmd/htpasswd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
Copyright 2026 Flant JSC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package cmd

import (
"github.com/spf13/cobra"
"k8s.io/kubectl/pkg/util/templates"

"github.com/deckhouse/deckhouse-cli/internal/tools/htpasswd"
)

// htpasswdLong is a verbatim string rather than templates.LongDesc so the
// usage forms and the algorithm table keep their line breaks (LongDesc reflows
// text and would collapse them into paragraphs).
const htpasswdLong = `Manage password files and hash passwords, a self-contained analog of Apache htpasswd. No external htpasswd binary is required.

Usage mirrors htpasswd:

d8 tools htpasswd [-cbdps... -C cost -r rounds] passwordfile username
d8 tools htpasswd -b [...] passwordfile username password
d8 tools htpasswd -n [-bmBdps...] [username]
d8 tools htpasswd -D passwordfile username
d8 tools htpasswd -v passwordfile username

Algorithms (choose one; default bcrypt):

-B bcrypt secure, the default
-m apr1 MD5 Apache MD5, legacy htpasswd default
-2 SHA-256 crypt secure
-5 SHA-512 crypt secure
-d CRYPT (DES) INSECURE, only first 8 chars used
-s SHA-1 INSECURE, unsalted
-p plaintext INSECURE, no hashing

Unlike Apache htpasswd (apr1 at cost 5 by default), d8 defaults to bcrypt at cost 10, so 'd8 tools htpasswd -n <username>' produces a strong hash ready for 'd8 iam user create/reset-password --password-hash'. With -n and no username the bare hash is printed, which is exactly what --password-hash expects. The -2 and -5 SHA-crypt algorithms and the -r rounds flag are d8 extensions; Apache htpasswd has no -2, -5, or -r flag.

© Flant JSC 2026`

var htpasswdExample = templates.Examples(`
# Create a file and add a user (prompts for the password)
d8 tools htpasswd -c users.htpasswd alice

# Add/update a user non-interactively (password from stdin)
echo -n 'S3cret!' | d8 tools htpasswd -i users.htpasswd bob

# Verify a password, then delete the user
d8 tools htpasswd -bv users.htpasswd bob 'S3cret!'
d8 tools htpasswd -D users.htpasswd bob

# Print a bcrypt hash for 'd8 iam user ... --password-hash'
HASH="$(echo -n 'Test12345!' | d8 tools htpasswd -ni)"
d8 iam user reset-password test-user --password-hash "$HASH"

# Other algorithms: apr1 line for an Ingress basic-auth secret; SHA-512 crypt
d8 tools htpasswd -nbm admin 'S3cret!'
d8 tools htpasswd -nb5 -r 100000 admin 'S3cret!'`)

func NewCommand() *cobra.Command {
htpasswdCmd := &cobra.Command{
Use: "htpasswd [flags] [passwordfile] [username] [password]",
Short: "Manage password files and hash passwords (Apache htpasswd analog)",
Long: htpasswdLong,
Example: htpasswdExample,
Args: cobra.ArbitraryArgs,
SilenceUsage: true,
SilenceErrors: false,
RunE: htpasswd.Htpasswd,
}

addFlags(htpasswdCmd.Flags())

return htpasswdCmd
}
Loading
Loading