Merge pull request #753 from RamXX/fix/dashboard-password-argon2

Replace SHA256 password hashing with Argon2id for dashboard auth
This commit is contained in:
Jaber Jaber
2026-03-31 02:22:02 +03:00
committed by GitHub
9 changed files with 260 additions and 91 deletions

View File

@@ -5,6 +5,16 @@ All notable changes to OpenFang will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Changed
- **BREAKING:** Dashboard password hashing switched from SHA256 to Argon2id. Existing `password_hash` values in `config.toml` must be regenerated with `openfang auth hash-password`. Only affects users with `[auth] enabled = true`.
### Fixed
- Dashboard passwords were hashed with plain SHA256 (no salt), making them vulnerable to rainbow table and GPU-accelerated brute force attacks. Now uses Argon2id with random salts.
## [0.1.0] - 2026-02-24
### Added

183
Cargo.lock generated
View File

@@ -730,9 +730,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.57"
version = "1.2.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -884,9 +884,9 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "cmake"
version = "0.1.57"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d"
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
dependencies = [
"cc",
]
@@ -1641,17 +1641,17 @@ dependencies = [
[[package]]
name = "dom_query"
version = "0.25.1"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d9c2e7f1d22d0f2ce07626d259b8a55f4a47cb0938d4006dd8ae037f17d585e"
checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89"
dependencies = [
"bit-set",
"cssparser 0.36.0",
"foldhash 0.2.0",
"html5ever 0.36.1",
"html5ever 0.38.0",
"precomputed-hash",
"selectors 0.35.0",
"tendril",
"selectors 0.36.1",
"tendril 0.5.0",
]
[[package]]
@@ -1739,9 +1739,9 @@ checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
[[package]]
name = "embed-resource"
version = "3.0.7"
version = "3.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47ec73ddcf6b7f23173d5c3c5a32b5507dc0a734de7730aa14abc5d5e296bb5f"
checksum = "63a1d0de4f2249aa0ff5884d7080814f446bb241a559af6c170a41e878ed2d45"
dependencies = [
"cc",
"memchr",
@@ -2651,12 +2651,12 @@ dependencies = [
[[package]]
name = "html5ever"
version = "0.36.1"
version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6452c4751a24e1b99c3260d505eaeee76a050573e61f30ac2c924ddc7236f01e"
checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2"
dependencies = [
"log",
"markup5ever 0.36.1",
"markup5ever 0.38.0",
]
[[package]]
@@ -3019,9 +3019,9 @@ dependencies = [
[[package]]
name = "instability"
version = "0.3.11"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d"
checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971"
dependencies = [
"darling",
"indoc",
@@ -3038,9 +3038,9 @@ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
[[package]]
name = "iri-string"
version = "0.7.10"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a"
checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb"
dependencies = [
"memchr",
"serde",
@@ -3091,9 +3091,9 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.17"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "ittapi"
@@ -3147,7 +3147,7 @@ dependencies = [
"cesu8",
"cfg-if",
"combine",
"jni-sys",
"jni-sys 0.3.1",
"log",
"thiserror 1.0.69",
"walkdir",
@@ -3156,9 +3156,31 @@ dependencies = [
[[package]]
name = "jni-sys"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
dependencies = [
"jni-sys 0.4.1",
]
[[package]]
name = "jni-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
dependencies = [
"jni-sys-macros",
]
[[package]]
name = "jni-sys-macros"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
dependencies = [
"quote",
"syn 2.0.117",
]
[[package]]
name = "jobserver"
@@ -3338,9 +3360,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "libredox"
version = "0.1.14"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08"
dependencies = [
"bitflags 2.11.0",
"libc",
@@ -3456,17 +3478,17 @@ dependencies = [
"phf_codegen 0.11.3",
"string_cache 0.8.9",
"string_cache_codegen 0.5.4",
"tendril",
"tendril 0.4.3",
]
[[package]]
name = "markup5ever"
version = "0.36.1"
version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c3294c4d74d0742910f8c7b466f44dda9eb2d5742c1e430138df290a1e8451c"
checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862"
dependencies = [
"log",
"tendril",
"tendril 0.5.0",
"web_atoms",
]
@@ -3560,9 +3582,9 @@ dependencies = [
[[package]]
name = "mio"
version = "1.1.1"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
dependencies = [
"libc",
"log",
@@ -3642,7 +3664,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
dependencies = [
"bitflags 2.11.0",
"jni-sys",
"jni-sys 0.3.1",
"log",
"ndk-sys",
"num_enum",
@@ -3662,7 +3684,7 @@ version = "0.6.0+11769913"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
dependencies = [
"jni-sys",
"jni-sys 0.3.1",
]
[[package]]
@@ -3740,9 +3762,9 @@ dependencies = [
[[package]]
name = "num-conv"
version = "0.2.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
[[package]]
name = "num-traits"
@@ -3957,6 +3979,7 @@ dependencies = [
name = "openfang-api"
version = "0.5.5"
dependencies = [
"argon2",
"async-trait",
"axum",
"base64 0.22.1",
@@ -3976,6 +3999,7 @@ dependencies = [
"openfang-skills",
"openfang-types",
"openfang-wire",
"rand 0.8.5",
"reqwest 0.12.28",
"serde",
"serde_json",
@@ -4943,7 +4967,7 @@ version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
dependencies = [
"toml_edit 0.25.4+spec-1.1.0",
"toml_edit 0.25.8+spec-1.1.0",
]
[[package]]
@@ -5161,9 +5185,9 @@ dependencies = [
[[package]]
name = "quoted_printable"
version = "0.5.1"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73"
checksum = "478e0585659a122aa407eb7e3c0e1fa51b1d8a870038bd29f0cf4a8551eea972"
[[package]]
name = "r-efi"
@@ -5584,9 +5608,9 @@ dependencies = [
[[package]]
name = "rmcp"
version = "1.2.0"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba6b9d2f0efe2258b23767f1f9e0054cfbcac9c2d6f81a031214143096d7864f"
checksum = "2231b2c085b371c01bc90c0e6c1cab8834711b6394533375bdbf870b0166d419"
dependencies = [
"async-trait",
"chrono",
@@ -5980,9 +6004,9 @@ dependencies = [
[[package]]
name = "selectors"
version = "0.35.0"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fdfed56cd634f04fe8b9ddf947ae3dc493483e819593d2ba17df9ad05db8b2"
checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c"
dependencies = [
"bitflags 2.11.0",
"cssparser 0.36.0",
@@ -6106,9 +6130,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "1.0.4"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98"
dependencies = [
"serde_core",
]
@@ -6317,9 +6341,9 @@ dependencies = [
[[package]]
name = "simd-adler32"
version = "0.3.8"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "siphasher"
@@ -6643,9 +6667,9 @@ dependencies = [
[[package]]
name = "tao"
version = "0.34.6"
version = "0.34.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d52c379e63da659a483a958110bbde891695a0ecb53e48cc7786d5eda7bb"
checksum = "9103edf55f2da3c82aea4c7fab7c4241032bfeea0e71fa557d98e00e7ce7cc20"
dependencies = [
"bitflags 2.11.0",
"block2",
@@ -7138,6 +7162,16 @@ dependencies = [
"utf-8",
]
[[package]]
name = "tendril"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24"
dependencies = [
"new_debug_unreachable",
"utf-8",
]
[[package]]
name = "termcolor"
version = "1.4.1"
@@ -7384,7 +7418,7 @@ checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
dependencies = [
"indexmap 2.13.0",
"serde_core",
"serde_spanned 1.0.4",
"serde_spanned 1.1.0",
"toml_datetime 0.7.5+spec-1.1.0",
"toml_parser",
"toml_writer",
@@ -7411,9 +7445,9 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "1.0.0+spec-1.1.0"
version = "1.1.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f"
dependencies = [
"serde_core",
]
@@ -7444,30 +7478,30 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.25.4+spec-1.1.0"
version = "0.25.8+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2"
checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c"
dependencies = [
"indexmap 2.13.0",
"toml_datetime 1.0.0+spec-1.1.0",
"toml_datetime 1.1.0+spec-1.1.0",
"toml_parser",
"winnow 0.7.15",
"winnow 1.0.0",
]
[[package]]
name = "toml_parser"
version = "1.0.9+spec-1.1.0"
version = "1.1.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011"
dependencies = [
"winnow 0.7.15",
"winnow 1.0.0",
]
[[package]]
name = "toml_writer"
version = "1.0.6+spec-1.1.0"
version = "1.1.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed"
[[package]]
name = "tower"
@@ -7745,9 +7779,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
[[package]]
name = "unicode-truncate"
@@ -7851,9 +7885,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.22.0"
version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
dependencies = [
"getrandom 0.4.2",
"js-sys",
@@ -9094,6 +9128,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "winnow"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.10.1"
@@ -9227,9 +9270,9 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
name = "wry"
version = "0.54.3"
version = "0.54.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a24eda84b5d488f99344e54b807138896cee8df0b2d16c793f1f6b80e6d8df1f"
checksum = "e5a8135d8676225e5744de000d4dff5a082501bf7db6a1c1495034f8c314edbc"
dependencies = [
"base64 0.22.1",
"block2",
@@ -9413,18 +9456,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.42"
version = "0.8.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3"
checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.42"
version = "0.8.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f"
checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -38,6 +38,8 @@ hmac = { workspace = true }
hex = { workspace = true }
socket2 = { workspace = true }
reqwest = { workspace = true }
argon2 = { workspace = true }
rand = { workspace = true }
[dev-dependencies]
tokio-test = { workspace = true }

View File

@@ -104,6 +104,15 @@ pub async fn build_router(
.allow_headers(tower_http::cors::Any)
};
// Warn if dashboard auth is enabled but the password hash is not Argon2id.
let ph = &state.kernel.config.auth.password_hash;
if state.kernel.config.auth.enabled && !ph.is_empty() && !ph.starts_with("$argon2") {
tracing::warn!(
"Dashboard auth password_hash is not in Argon2id format. \
Login will fail. Regenerate with: openfang auth hash-password"
);
}
// Trim whitespace so `api_key = ""` or `api_key = " "` both disable auth.
let api_key = state.kernel.config.api_key.trim().to_string();
let auth_state = crate::middleware::AuthState {

View File

@@ -55,20 +55,27 @@ pub fn verify_session_token(token: &str, secret: &str) -> Option<String> {
}
}
/// Hash a password with SHA256 for config storage.
/// Hash a password with Argon2id for config storage.
///
/// Returns a PHC-format string (e.g. `$argon2id$v=19$m=19456,t=2,p=1$...`).
pub fn hash_password(password: &str) -> String {
use sha2::Digest;
hex::encode(Sha256::digest(password.as_bytes()))
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
let salt = SaltString::generate(&mut rand::thread_rng());
Argon2::default()
.hash_password(password.as_bytes(), &salt)
.expect("Argon2 hashing should not fail with valid inputs")
.to_string()
}
/// Verify a password against a stored SHA256 hash (constant-time).
/// Verify a password against a stored Argon2id hash (PHC string format).
pub fn verify_password(password: &str, stored_hash: &str) -> bool {
let computed = hash_password(password);
use subtle::ConstantTimeEq;
if computed.len() != stored_hash.len() {
use argon2::{password_hash::PasswordHash, Argon2, PasswordVerifier};
let Ok(parsed) = PasswordHash::new(stored_hash) else {
return false;
}
computed.as_bytes().ct_eq(stored_hash.as_bytes()).into()
};
Argon2::default()
.verify_password(password.as_bytes(), &parsed)
.is_ok()
}
#[cfg(test)]
@@ -78,10 +85,31 @@ mod tests {
#[test]
fn test_hash_and_verify_password() {
let hash = hash_password("secret123");
assert!(
hash.starts_with("$argon2id$"),
"should produce Argon2id PHC string"
);
assert!(verify_password("secret123", &hash));
assert!(!verify_password("wrong", &hash));
}
#[test]
fn test_hash_produces_unique_salts() {
let h1 = hash_password("same");
let h2 = hash_password("same");
assert_ne!(h1, h2, "each hash should use a unique salt");
assert!(verify_password("same", &h1));
assert!(verify_password("same", &h2));
}
#[test]
fn test_rejects_non_argon2_hash() {
// A plain SHA256 hex string should no longer be accepted.
use sha2::Digest;
let sha256_hash = hex::encode(sha2::Sha256::digest(b"password"));
assert!(!verify_password("password", &sha256_hash));
}
#[test]
fn test_create_and_verify_token() {
let token = create_session_token("admin", "my-secret", 1);
@@ -103,7 +131,14 @@ mod tests {
}
#[test]
fn test_password_hash_length_mismatch() {
fn test_rejects_garbage_input() {
assert!(!verify_password("x", "short"));
assert!(!verify_password("x", ""));
}
#[test]
fn test_verify_malformed_argon2_hash() {
// Starts with $argon2 but is not a valid PHC string.
assert!(!verify_password("x", "$argon2id$garbage"));
}
}

View File

@@ -237,6 +237,9 @@ enum Commands {
#[arg(long)]
json: bool,
},
/// Dashboard authentication [*].
#[command(subcommand)]
Auth(AuthCommands),
/// Security tools and audit trail [*].
#[command(subcommand)]
Security(SecurityCommands),
@@ -679,6 +682,12 @@ enum CronCommands {
},
}
#[derive(Subcommand)]
enum AuthCommands {
/// Generate an Argon2id password hash for dashboard authentication.
HashPassword,
}
#[derive(Subcommand)]
enum SecurityCommands {
/// Show security status summary.
@@ -1057,6 +1066,9 @@ fn main() {
Some(Commands::Sessions { agent, json }) => cmd_sessions(agent.as_deref(), json),
Some(Commands::Logs { lines, follow }) => cmd_logs(lines, follow),
Some(Commands::Health { json }) => cmd_health(json),
Some(Commands::Auth(sub)) => match sub {
AuthCommands::HashPassword => cmd_auth_hash_password(),
},
Some(Commands::Security(sub)) => match sub {
SecurityCommands::Status { json } => cmd_security_status(json),
SecurityCommands::Audit { limit, json } => cmd_security_audit(limit, json),
@@ -5985,6 +5997,28 @@ fn cmd_health(json: bool) {
}
}
fn cmd_auth_hash_password() {
let password = prompt_input("Enter password: ");
if password.is_empty() {
ui::error("Empty password.");
std::process::exit(1);
}
let confirm = prompt_input("Confirm password: ");
if password != confirm {
ui::error("Passwords do not match.");
std::process::exit(1);
}
let hash = openfang_api::session_auth::hash_password(&password);
println!();
ui::success("Argon2id hash generated. Add this to your config.toml:");
println!();
println!(" [auth]");
println!(" enabled = true");
println!(" password_hash = \"{}\"", hash);
println!();
ui::hint("Restart the daemon after updating config.toml");
}
fn cmd_security_status(json: bool) {
let base = require_daemon("security status");
let client = daemon_client();

View File

@@ -1161,7 +1161,7 @@ pub struct AuthConfig {
pub enabled: bool,
/// Admin username.
pub username: String,
/// SHA256 hash of the password (hex-encoded).
/// Argon2id password hash (PHC string format).
/// Generate with: openfang auth hash-password
pub password_hash: String,
/// Session token lifetime in hours (default: 168 = 7 days).

View File

@@ -318,6 +318,37 @@ shared_secret = "my-cluster-secret"
---
### `[auth]`
Configures dashboard login with username/password authentication. Disabled by default.
```toml
[auth]
enabled = true
username = "admin"
password_hash = "$argon2id$v=19$m=19456,t=2,p=1$..." # generate with: openfang auth hash-password
session_ttl_hours = 168
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | bool | `false` | Enable username/password authentication for the dashboard. |
| `username` | string | `"admin"` | Admin username. |
| `password_hash` | string | `""` (empty) | Argon2id password hash in PHC string format. Generate with `openfang auth hash-password`. |
| `session_ttl_hours` | u64 | `168` (7 days) | Session token lifetime in hours. |
**Generating a password hash:**
```bash
openfang auth hash-password
```
This prompts for a password and outputs an Argon2id PHC string to paste into `config.toml`.
> **Breaking change (v0.5.0):** Password hashes must be in Argon2id format. Older SHA256 hex hashes from versions prior to v0.5.0 are no longer accepted. Re-run `openfang auth hash-password` to generate a new hash.
---
### `[web]`
Configures web search and web fetch capabilities used by agent tools.

View File

@@ -583,19 +583,24 @@ docker run -d --name openfang \
### How do I protect the dashboard with a password?
OpenFang doesn't have built-in login. Use a reverse proxy with basic auth:
OpenFang has built-in dashboard authentication. Enable it in `~/.openfang/config.toml`:
**Caddy example:**
```
ai.yourdomain.com {
basicauth {
username $2a$14$YOUR_HASHED_PASSWORD
}
reverse_proxy localhost:4200
}
```toml
[auth]
enabled = true
username = "admin"
password_hash = "$argon2id$..." # see below
```
Generate a password hash: `caddy hash-password`
Generate the password hash:
```bash
openfang auth hash-password
```
Paste the output into the `password_hash` field and restart the daemon.
For public-facing deployments, you should also place a reverse proxy (Caddy, nginx) in front for TLS termination.
### How do I configure the embedding model for memory?