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
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,33 @@ configuration settings stored in the `sshd_config` file. The resource can:
- The resource requires OpenSSH server to be installed on the system.
- The resource must run in a process context that has permissions to read and write the `sshd_config`
file.
- Configuration operations invoke `sshd -T`, which requires usable SSH server host keys. An elevated
process can still fail if those keys are missing or inaccessible.
- On Windows, the default configuration file path is `%ProgramData%\ssh\sshd_config`.
- On Linux, the default configuration file path is `/etc/ssh/sshd_config`.
- To manage a configuration file in a non-default location, specify the
[sshd_config_filepath](#sshd_config_filepath) property for the instance.

### Troubleshooting configuration validation

When `sshd -T` fails, the resource returns the original OpenSSH diagnostic. To inspect the failure
directly, run `sshd -T` in the same process context. For a custom configuration, run
`sshd -T -f <configuration-path>`.

The message `sshd: no hostkeys available -- exiting` means that OpenSSH couldn't load a usable host
key. Check the preceding diagnostics to distinguish missing keys from keys that can't be read.
Elevation alone doesn't create missing host keys.

On Windows, starting the installed `sshd` service initializes missing default host keys. If you need
to initialize the keys without starting the service, an administrator can run `ssh-keygen -A` to
generate missing default host keys. These are explicit setup actions; the resource doesn't generate
keys or start the service. If the configuration specifies custom `HostKey` paths, check those paths
instead.

For `Permission denied` errors, check the permissions on the configuration file or host key named in
the diagnostic and the identity running DSC. Don't replace existing host keys or relax their
permissions as a general workaround.

## Capabilities

The resource has the following capabilities:
Expand Down
1 change: 0 additions & 1 deletion resources/sshdconfig/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,5 @@ seededConfigFromDefault = "Seeded missing sshd_config from '%{source}' to '%{tar
sshdConfigDefaultNotFound = "'%{path}' does not exist and no default source could be found. Checked: %{paths}. Start the sshd service to initialize it, then retry."
sshdConfigNotFoundNonWindows = "'%{path}' does not exist. Start the sshd service to initialize it, then retry."
sshdConfigReadFailed = "failed to read sshd_config at path: '%{path}'"
sshdElevation = "elevated security context required"
tempFileCreated = "temporary file created at: %{path}"
tracingInitError = "Failed to initialize tracing"
53 changes: 47 additions & 6 deletions resources/sshdconfig/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

use rust_i18n::t;
use serde_json::{Map, Value};
use std::{path::PathBuf, process::Command};
use std::{path::PathBuf, process::{Command, Output}};
use tracing::{debug, warn, Level};
use tracing_subscriber::{EnvFilter, Layer, prelude::__tracing_subscriber_SubscriberExt};

Expand Down Expand Up @@ -194,18 +194,17 @@ pub fn invoke_sshd_config_validation(args: Option<SshdCommandArgs>) -> Result<St
let output = command.output()
.map_err(|e| SshdConfigError::CommandError(e.to_string()))?;

parse_sshd_output(output)
}

fn parse_sshd_output(output: Output) -> Result<String, SshdConfigError> {
if output.status.success() {
let stdout = String::from_utf8(output.stdout)
.map_err(|e| SshdConfigError::CommandError(e.to_string()))?;
Ok(stdout)
} else {
let stderr = String::from_utf8(output.stderr)
.map_err(|e| SshdConfigError::CommandError(e.to_string()))?;
if stderr.contains("sshd: no hostkeys available") || stderr.contains("Permission denied") {
return Err(SshdConfigError::CommandError(
t!("util.sshdElevation").to_string()
));
}
Err(SshdConfigError::CommandError(stderr))
}
}
Expand Down Expand Up @@ -318,3 +317,45 @@ pub fn read_sshd_config(input: Option<PathBuf>) -> Result<String, SshdConfigErro
Err(SshdConfigError::FileNotFound(filepath.display().to_string()))
}
}

#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
use std::os::unix::process::ExitStatusExt;
#[cfg(windows)]
use std::os::windows::process::ExitStatusExt;
use std::process::ExitStatus;

#[test]
fn preserves_sshd_failure_diagnostics() {
for stderr in [
"Unable to load host key: missing_key\nsshd: no hostkeys available -- exiting.\n",
"Unable to load host key: protected_key: Permission denied\n",
"sshd_config line 1: Bad configuration option: InvalidOption\n",
] {
let output = Output {
status: ExitStatus::from_raw(1),
stdout: Vec::new(),
stderr: stderr.as_bytes().to_vec(),
};

match parse_sshd_output(output) {
Err(SshdConfigError::CommandError(message)) => assert_eq!(message, stderr),
result => panic!("Expected original sshd diagnostic, got {result:?}"),
}
}
}

#[test]
fn returns_sshd_stdout_on_success() {
let stdout = "port 22\npasswordauthentication yes\n";
let output = Output {
status: ExitStatus::from_raw(0),
stdout: stdout.as_bytes().to_vec(),
stderr: b"diagnostic on stderr\n".to_vec(),
};

assert_eq!(parse_sshd_output(output).unwrap(), stdout);
}
}
Loading