diff --git a/builds/misc/images.yaml b/builds/misc/images.yaml
index 55fe94da8c0..72bcc5a109b 100644
--- a/builds/misc/images.yaml
+++ b/builds/misc/images.yaml
@@ -19,15 +19,39 @@ jobs:
       - script: scripts/linux/installPrereqs.sh -u $(NetCorePackageUri)
         name: install
         displayName: Install dependencies
+
+      - bash: 'docker login $(registry.address) --username $(registry.user) --password $(registry.password)'
+        displayName: 'Docker Login'
+
+      - script: edgelet/build/linux/install.sh --package-arm
+        displayName: Install Rust
+
+      - bash: 'echo "##vso[task.setvariable variable=PATH;]$HOME/.cargo/bin:$PATH"'
+        displayName: Modify path
+
+      - bash: 'cargo install --git https://github.com/arsing/cross.git --branch set-path'
+        displayName: 'Install cross (fork with docker fix)'
+
       - script: scripts/linux/buildBranch.sh -c Release --no-rocksdb-bin
         name: build
         displayName: Build (release)
+
+      - script: scripts/linux/buildDiagnostics.sh
+        displayName: Build iotedge-diagnostics
+
       - task: PublishBuildArtifacts@1
         displayName: 'Publish Artifacts'
         inputs:
           PathtoPublish: '$(Build.BinariesDirectory)/publish'
           ArtifactName: 'core-linux'
 
+      # azureiotedge-diagnostics
+      - template: templates/image-linux.yaml
+        parameters:
+          name: azureiotedge-diagnostics
+          imageName: azureiotedge-diagnostics
+          project: azureiotedge-diagnostics
+
       # Edge Agent
       - template: templates/image-linux.yaml
         parameters:
@@ -106,16 +130,30 @@ jobs:
       - powershell: scripts/windows/setup/Install-Prerequisites.ps1 -DotnetSdkUrl $(NetCorePackageUri) -Dotnet -Nuget
         name: install
         displayName: Install
+
+      - script: echo $(registry.password)|docker login "edgebuilds.azurecr.io" -u "$(registry.user)" --password-stdin
+        displayName: Docker Login
+
       - powershell: scripts/windows/build/Publish-Branch.ps1 -Configuration:"Release" -PublishTests:$False -UpdateVersion
         name: build
         displayName: Build
+
+      - powershell: edgelet/build/windows/build-diagnostics.ps1
+        displayName: Build iotedge-diagnostics
+
       - task: PublishBuildArtifacts@1
         displayName: 'Publish Artifacts'
         inputs:
           PathtoPublish: '$(Build.BinariesDirectory)/publish'
           ArtifactName: 'core-windows'
-      - script: echo $(registry.password)|docker login "edgebuilds.azurecr.io" -u "$(registry.user)" --password-stdin
-        displayName: Docker Login
+
+      # azureiotedge-diagnostics
+      - template: templates/image-windows.yaml
+        parameters:
+          name: azureiotedge-diagnostics
+          imageName: azureiotedge-diagnostics
+          project: azureiotedge-diagnostics
+          arm32v7: 'false'
 
       # Edge Agent
       - template: templates/image-windows.yaml
@@ -201,4 +239,6 @@ jobs:
     - script: scripts/linux/buildManifest.sh -r $(registry.address) -u $(registry.user) -p $(registry.password) -v $(Build.BuildNumber) -t $(System.DefaultWorkingDirectory)/edge-hub/docker/manifest.yaml.template -n microsoft --tags "$(tags)"
       displayName: 'Publish Edge Hub Manifest'
     - script: scripts/linux/buildManifest.sh -r $(registry.address) -u $(registry.user) -p $(registry.password) -v $(Build.BuildNumber) -t $(System.DefaultWorkingDirectory)/edge-modules/SimulatedTemperatureSensor/docker/manifest.yaml.template -n microsoft --tags "$(tags)"
-      displayName: 'Publish Temperature Sensor Manifest'
\ No newline at end of file
+      displayName: 'Publish Temperature Sensor Manifest'
+    - script: scripts/linux/buildManifest.sh -r $(registry.address) -u $(registry.user) -p $(registry.password) -v $(Build.BuildNumber) -t $(System.DefaultWorkingDirectory)/edgelet/iotedge-diagnostics/docker/manifest.yaml.template -n microsoft --tags "$(tags)"
+      displayName: 'Publish azureiotedge-diagnostics Manifest'
diff --git a/edgelet/Cargo.lock b/edgelet/Cargo.lock
index 12386b68c2c..83b079c5755 100755
--- a/edgelet/Cargo.lock
+++ b/edgelet/Cargo.lock
@@ -323,6 +323,27 @@ name = "dtoa"
 version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
+[[package]]
+name = "edgelet-config"
+version = "0.1.0"
+dependencies = [
+ "base64 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "config 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "edgelet-core 0.1.0",
+ "edgelet-docker 0.1.0",
+ "edgelet-utils 0.1.0",
+ "failure 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_derive 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "sha2 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "url_serde 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "edgelet-core"
 version = "0.1.0"
@@ -837,15 +858,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 name = "iotedge"
 version = "0.1.0"
 dependencies = [
+ "atty 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
  "bytes 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "chrono-humanize 0.0.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "docker 0.1.0",
+ "edgelet-config 0.1.0",
  "edgelet-core 0.1.0",
+ "edgelet-docker 0.1.0",
+ "edgelet-http 0.1.0",
  "edgelet-http-mgmt 0.1.0",
  "failure 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "futures 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)",
+ "hyper 0.12.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
+ "management 0.1.0",
+ "mini-sntp 0.1.0",
+ "native-tls 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "openssl 0.10.12 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_derive 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
  "tabwriter 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "termcolor 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "iotedge-diagnostics"
+version = "0.1.0"
+dependencies = [
+ "clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "edgelet-core 0.1.0",
+ "futures 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)",
+ "hyper 0.12.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "hyperlocal 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "hyperlocal-windows 0.1.0 (git+https://github.com/Azure/hyperlocal-windows)",
+ "management 0.1.0",
+ "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
@@ -856,9 +910,9 @@ version = "0.1.0"
 dependencies = [
  "base64 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "config 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "docker 0.1.0",
  "dps 0.1.0",
+ "edgelet-config 0.1.0",
  "edgelet-core 0.1.0",
  "edgelet-docker 0.1.0",
  "edgelet-hsm 0.1.0",
@@ -876,15 +930,12 @@ dependencies = [
  "iothubservice 0.1.0",
  "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "provisioning 0.1.0",
- "serde 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_derive 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
  "sha2 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-signal 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "url_serde 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "win-logger 0.1.0",
  "windows-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
@@ -1085,6 +1136,14 @@ dependencies = [
  "unicase 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "mini-sntp"
+version = "0.1.0"
+dependencies = [
+ "chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "failure 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "mio"
 version = "0.6.14"
@@ -1298,9 +1357,9 @@ dependencies = [
 name = "provisioning"
 version = "0.1.0"
 dependencies = [
- "base64 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "bytes 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "dps 0.1.0",
+ "edgelet-config 0.1.0",
  "edgelet-core 0.1.0",
  "edgelet-hsm 0.1.0",
  "edgelet-http 0.1.0",
@@ -1309,7 +1368,6 @@ dependencies = [
  "futures 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)",
  "hsm 0.1.0",
  "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde_derive 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
diff --git a/edgelet/Cargo.toml b/edgelet/Cargo.toml
index bae7759297d..f86c77d66d5 100644
--- a/edgelet/Cargo.toml
+++ b/edgelet/Cargo.toml
@@ -2,6 +2,7 @@
 members = [
     "docker-rs",
     "dps",
+    "edgelet-config",
     "edgelet-core",
     "edgelet-docker",
     "edgelet-hsm",
@@ -19,8 +20,10 @@ members = [
     "iotedge",
     "iotedged",
     "iotedged-eventlog-messages",
+    "iotedge-diagnostics",
     "iothubservice",
     "management",
+    "mini-sntp",
     "provisioning",
     "systemd",
     "tokio-named-pipe",
diff --git a/edgelet/Cross.toml b/edgelet/Cross.toml
index 2dd31063a3e..045c5ee96e7 100644
--- a/edgelet/Cross.toml
+++ b/edgelet/Cross.toml
@@ -7,3 +7,8 @@ passthrough = [
 
 [target.armv7-unknown-linux-gnueabihf]
 image = "azureiotedge/gcc-linaro-7.2.1-2017.11-x86_64_arm-linux-gnueabihf:0.2"
+
+[target.armv7-unknown-linux-musleabihf]
+# Built from rust-embedded/cross#718a19cd68fb09428532d1317515fe7303692b47 with `./build-docker-image.sh armv7-unknown-linux-musleabihf`
+# because the image in Docker hub is outdated and broken
+image = "azureiotedge/armv7-unknown-linux-musleabihf:0.1"
diff --git a/edgelet/build/windows/build-diagnostics.ps1 b/edgelet/build/windows/build-diagnostics.ps1
new file mode 100644
index 00000000000..fcddf9a3e4f
--- /dev/null
+++ b/edgelet/build/windows/build-diagnostics.ps1
@@ -0,0 +1,46 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+<#
+ # Builds and publishes to target/publish/ the iotedge-diagnostics binary and its associated dockerfile
+ #>
+
+$ErrorActionPreference = 'Continue'
+
+. (Join-Path $PSScriptRoot 'util.ps1')
+
+Assert-Rust
+
+$cargo = Get-CargoCommand
+$ManifestPath = Get-Manifest
+
+$versionInfoFilePath = Join-Path $env:BUILD_REPOSITORY_LOCALPATH 'versionInfo.json'
+$env:VERSION = Get-Content $versionInfoFilePath | ConvertFrom-JSON | % version
+$env:NO_VALGRIND = 'true'
+
+$originalRustflags = $env:RUSTFLAGS
+$env:RUSTFLAGS += ' -C target-feature=+crt-static'
+Write-Host "$cargo build -p iotedge-diagnostics --release --manifest-path $ManifestPath"
+Invoke-Expression "$cargo build -p iotedge-diagnostics --release --manifest-path $ManifestPath"
+if ($originalRustflags -eq '') {
+    Remove-Item Env:\RUSTFLAGS
+}
+else {
+    $env:RUSTFLAGS = $originalRustflags
+}
+if ($LastExitCode) {
+    Throw "cargo build failed with exit code $LastExitCode"
+}
+
+$ErrorActionPreference = 'Stop'
+
+$publishFolder = [IO.Path]::Combine($env:BUILD_BINARIESDIRECTORY, 'publish', 'azureiotedge-diagnostics')
+
+New-Item -Type Directory $publishFolder
+
+Copy-Item -Recurse `
+    ([IO.Path]::Combine($env:BUILD_REPOSITORY_LOCALPATH, 'edgelet', 'iotedge-diagnostics', 'docker')) `
+    ([IO.Path]::Combine($publishFolder, 'docker'))
+
+Copy-Item `
+    ([IO.Path]::Combine($env:BUILD_REPOSITORY_LOCALPATH, 'edgelet', 'target', 'release', 'iotedge-diagnostics.exe')) `
+    ([IO.Path]::Combine($publishFolder, 'docker', 'windows', 'amd64'))
diff --git a/edgelet/build/windows/iotedge.wm.xml b/edgelet/build/windows/iotedge.wm.xml
index 9590823f55c..0560150680c 100644
--- a/edgelet/build/windows/iotedge.wm.xml
+++ b/edgelet/build/windows/iotedge.wm.xml
@@ -15,6 +15,7 @@
     <file source="$(_REPO_ROOT)\edgelet\target\release\iotedged_eventlog_messages.dll" destinationDir="$(runtime.programFiles)\iotedge" />
     <file source="$(_REPO_ROOT)\edgelet\hsm-sys\azure-iot-hsm-c\build\Release\iothsm.dll" destinationDir="$(runtime.programFiles)\iotedge" />
     <file source="$(_OPENSSL_ROOT_DIR)\bin\libeay32.dll" destinationDir="$(runtime.programFiles)\iotedge" />
+    <file source="$(_OPENSSL_ROOT_DIR)\bin\ssleay32.dll" destinationDir="$(runtime.programFiles)\iotedge" />
     <file source="$(_REPO_ROOT)\edgelet\contrib\docs\LICENSE" destinationDir="$(runtime.programFiles)\iotedge\LICENSE" />
     <file source="$(_REPO_ROOT)\edgelet\contrib\docs\ThirdPartyNotices" destinationDir="$(runtime.programFiles)\iotedge\LICENSE" />
     <file source="$(_REPO_ROOT)\edgelet\contrib\docs\trademark" destinationDir="$(runtime.programFiles)\iotedge\LICENSE" />
diff --git a/edgelet/build/windows/util.ps1 b/edgelet/build/windows/util.ps1
index 4dd6ad376c3..cced3ea9677 100644
--- a/edgelet/build/windows/util.ps1
+++ b/edgelet/build/windows/util.ps1
@@ -1,5 +1,8 @@
 # Copyright (c) Microsoft. All rights reserved.
 
+New-Item -Type Directory -Force '~/.cargo/bin'
+$env:PATH += ";$(Resolve-Path '~/.cargo/bin')"
+
 function Test-RustUp
 {
     (get-command -Name rustup.exe -ErrorAction SilentlyContinue) -ne $null
@@ -35,7 +38,7 @@ function Assert-Rust
             Throw "Failed to download rustup with exit code $LastExitCode"
         }
 
-        Write-Host "Running rustup.exe"
+        Write-Host "Running rustup-init.exe"
         ./rustup-init.exe -y --default-toolchain stable-x86_64-pc-windows-msvc
         if ($LastExitCode)
         {
diff --git a/edgelet/contrib/config/linux/config.yaml b/edgelet/contrib/config/linux/config.yaml
index 5d3af0e6bf5..4e59b5b8f94 100644
--- a/edgelet/contrib/config/linux/config.yaml
+++ b/edgelet/contrib/config/linux/config.yaml
@@ -102,7 +102,7 @@ hostname: "<ADD HOSTNAME HERE>"
 ###############################################################################
 #
 #
-#Configures URIs used by clients of the management and workload APIs
+# Configures URIs used by clients of the management and workload APIs
 #     management_uri - used by the Edge Agent and 'iotedge' CLI to start,
 #                      stop, and manage modules
 #     workload_uri   - used by modules to retrieve tokens and certificates
@@ -111,6 +111,11 @@ hostname: "<ADD HOSTNAME HERE>"
 #     http - connect over TCP
 #     unix - connect over Unix domain socket
 #
+# Note: When using the fd:// scheme for listen.management_uri or listen.workload_uri,
+# the path of connect.management_uri and connect.workload_uri must match
+# the path of the underlying socket in the systemd socket files
+# (iotedge.socket and iotedge.mgmt.socket).
+#
 ###############################################################################
 
 connect:
@@ -136,6 +141,11 @@ connect:
 #     listen address is fd://iotedge.workload,
 #     connect address is unix:///var/run/iotedge/workload.sock
 #
+# Note: When using the fd:// scheme for listen.management_uri or listen.workload_uri,
+# the path of connect.management_uri and connect.workload_uri must match
+# the path of the underlying socket in the systemd socket files
+# (iotedge.socket and iotedge.mgmt.socket).
+#
 ###############################################################################
 
 listen:
diff --git a/edgelet/edgelet-config/Cargo.toml b/edgelet/edgelet-config/Cargo.toml
new file mode 100644
index 00000000000..f434e951147
--- /dev/null
+++ b/edgelet/edgelet-config/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "edgelet-config"
+version = "0.1.0"
+authors = ["Azure IoT Edge Devs"]
+publish = false
+
+[dependencies]
+base64 = "0.9"
+config = "0.8"
+failure = "0.1"
+log = "0.4"
+regex = "0.2"
+serde = "1.0"
+serde_derive = "1.0"
+serde_json = "1.0"
+sha2 = "0.7.0"
+url = "1.7"
+url_serde = "0.2"
+
+edgelet-core = { path = "../edgelet-core" }
+edgelet-utils = { path = "../edgelet-utils" }
+
+[dev_dependencies]
+tempdir = "0.3.7"
+
+edgelet-docker = { path = "../edgelet-docker" }
diff --git a/edgelet/iotedged/src/config/unix/default.yaml b/edgelet/edgelet-config/config/unix/default.yaml
similarity index 100%
rename from edgelet/iotedged/src/config/unix/default.yaml
rename to edgelet/edgelet-config/config/unix/default.yaml
diff --git a/edgelet/iotedged/src/config/windows/default.yaml b/edgelet/edgelet-config/config/windows/default.yaml
similarity index 100%
rename from edgelet/iotedged/src/config/windows/default.yaml
rename to edgelet/edgelet-config/config/windows/default.yaml
diff --git a/edgelet/iotedged/src/settings.rs b/edgelet/edgelet-config/src/lib.rs
similarity index 57%
rename from edgelet/iotedged/src/settings.rs
rename to edgelet/edgelet-config/src/lib.rs
index ed47a294668..e2969b782cd 100644
--- a/edgelet/iotedged/src/settings.rs
+++ b/edgelet/edgelet-config/src/lib.rs
@@ -1,25 +1,53 @@
 // Copyright (c) Microsoft. All rights reserved.
 
-use std::fs::{File as FsFile, OpenOptions};
+#![deny(unused_extern_crates, warnings)]
+#![deny(clippy::all, clippy::pedantic)]
+#![allow(
+    clippy::default_trait_access,
+    clippy::doc_markdown, // clippy want the "IoT" of "IoT Hub" in a code fence
+    clippy::module_name_repetitions,
+    clippy::shadow_unrelated,
+    clippy::use_self,
+)]
+
+extern crate base64;
+extern crate config;
+extern crate failure;
+#[macro_use]
+extern crate log;
+extern crate regex;
+extern crate serde;
+extern crate sha2;
+#[macro_use]
+extern crate serde_derive;
+extern crate serde_json;
+#[cfg(test)]
+extern crate tempdir;
+extern crate url;
+extern crate url_serde;
+
+extern crate edgelet_core;
+#[cfg(test)]
+extern crate edgelet_docker;
+extern crate edgelet_utils;
+
+use std::fs::OpenOptions;
 use std::io::Read;
 use std::path::{Path, PathBuf};
 
-use base64;
 use config::{Config, Environment, File, FileFormat};
-use failure::{Fail, ResultExt};
+use failure::{Context, Fail};
 use log::Level;
+use regex::Regex;
 use serde::de::DeserializeOwned;
 use serde::Serialize;
-use serde_json;
 use sha2::{Digest, Sha256};
 use url::Url;
-use url_serde;
 
+use edgelet_core::crypto::MemoryKey;
 use edgelet_core::ModuleSpec;
 use edgelet_utils::log_failure;
 
-use error::{Error, ErrorKind, InitializeErrorReason};
-
 /// This is the name of the network created by the iotedged
 const DEFAULT_NETWORKID: &str = "azure-iot-edge";
 
@@ -27,10 +55,17 @@ const DEFAULT_NETWORKID: &str = "azure-iot-edge";
 pub const DEFAULT_CONNECTION_STRING: &str = "<ADD DEVICE CONNECTION STRING HERE>";
 
 #[cfg(unix)]
-static DEFAULTS: &str = include_str!("config/unix/default.yaml");
+const DEFAULTS: &str = include_str!("../config/unix/default.yaml");
 
 #[cfg(windows)]
-static DEFAULTS: &str = include_str!("config/windows/default.yaml");
+const DEFAULTS: &str = include_str!("../config/windows/default.yaml");
+
+const DEVICEID_KEY: &str = "DeviceId";
+const HOSTNAME_KEY: &str = "HostName";
+const SHAREDACCESSKEY_KEY: &str = "SharedAccessKey";
+
+const DEVICEID_REGEX: &str = r"^[A-Za-z0-9\-:.+%_#*?!(),=@;$']{1,128}$";
+const HOSTNAME_REGEX: &str = r"^[a-zA-Z0-9_\-\.]+$";
 
 #[derive(Debug, Deserialize, Serialize)]
 #[serde(rename_all = "lowercase")]
@@ -39,9 +74,74 @@ pub struct Manual {
 }
 
 impl Manual {
+    pub fn new(device_connection_string: String) -> Self {
+        Manual {
+            device_connection_string,
+        }
+    }
+
     pub fn device_connection_string(&self) -> &str {
         &self.device_connection_string
     }
+
+    pub fn parse_device_connection_string(
+        &self,
+    ) -> Result<(MemoryKey, String, String), ParseManualDeviceConnectionStringError> {
+        if self.device_connection_string.is_empty() {
+            return Err(ParseManualDeviceConnectionStringError::Empty);
+        }
+
+        let mut key = None;
+        let mut device_id = None;
+        let mut hub = None;
+
+        let parts: Vec<&str> = self.device_connection_string.split(';').collect();
+        for p in parts {
+            let s: Vec<&str> = p.split('=').collect();
+            match s[0] {
+                SHAREDACCESSKEY_KEY => key = Some(s[1].to_string()),
+                DEVICEID_KEY => device_id = Some(s[1].to_string()),
+                HOSTNAME_KEY => hub = Some(s[1].to_string()),
+                _ => (), // Ignore extraneous component in the connection string
+            }
+        }
+
+        let key = key.ok_or(
+            ParseManualDeviceConnectionStringError::MissingRequiredParameter(SHAREDACCESSKEY_KEY),
+        )?;
+        if key.is_empty() {
+            return Err(ParseManualDeviceConnectionStringError::MalformedParameter(
+                SHAREDACCESSKEY_KEY,
+            ));
+        }
+        let key = MemoryKey::new(base64::decode(&key).map_err(|_| {
+            ParseManualDeviceConnectionStringError::MalformedParameter(SHAREDACCESSKEY_KEY)
+        })?);
+
+        let device_id = device_id.ok_or(
+            ParseManualDeviceConnectionStringError::MissingRequiredParameter(DEVICEID_KEY),
+        )?;
+        let device_id_regex =
+            Regex::new(DEVICEID_REGEX).expect("This hard-coded regex is expected to be valid.");
+        if !device_id_regex.is_match(&device_id) {
+            return Err(ParseManualDeviceConnectionStringError::MalformedParameter(
+                DEVICEID_KEY,
+            ));
+        }
+
+        let hub = hub.ok_or(
+            ParseManualDeviceConnectionStringError::MissingRequiredParameter(HOSTNAME_KEY),
+        )?;
+        let hub_regex =
+            Regex::new(HOSTNAME_REGEX).expect("This hard-coded regex is expected to be valid.");
+        if !hub_regex.is_match(&hub) {
+            return Err(ParseManualDeviceConnectionStringError::MalformedParameter(
+                HOSTNAME_KEY,
+            ));
+        }
+
+        Ok((key, device_id.to_owned(), hub.to_owned()))
+    }
 }
 
 #[derive(Debug, Deserialize, Serialize)]
@@ -175,24 +275,24 @@ impl<T> Settings<T>
 where
     T: DeserializeOwned + Serialize,
 {
-    pub fn new(filename: Option<&str>) -> Result<Self, Error> {
+    pub fn new(filename: Option<&Path>) -> Result<Self, LoadSettingsError> {
+        let filename = filename.map(|filename| {
+            filename.to_str().unwrap_or_else(|| {
+                panic!(
+                    "cannot load config from {} because it is not a utf-8 path",
+                    filename.display()
+                )
+            })
+        });
         let mut config = Config::default();
-        config
-            .merge(File::from_str(DEFAULTS, FileFormat::Yaml))
-            .context(ErrorKind::Initialize(InitializeErrorReason::LoadSettings))?;
+        config.merge(File::from_str(DEFAULTS, FileFormat::Yaml))?;
         if let Some(file) = filename {
-            config
-                .merge(File::with_name(file).required(true))
-                .context(ErrorKind::Initialize(InitializeErrorReason::LoadSettings))?;
+            config.merge(File::with_name(file).required(true))?;
         }
 
-        config
-            .merge(Environment::with_prefix("iotedge"))
-            .context(ErrorKind::Initialize(InitializeErrorReason::LoadSettings))?;
+        config.merge(Environment::with_prefix("iotedge"))?;
 
-        let settings: Self = config
-            .try_into()
-            .context(ErrorKind::Initialize(InitializeErrorReason::LoadSettings))?;
+        let settings: Self = config.try_into()?;
 
         Ok(settings)
     }
@@ -233,39 +333,85 @@ where
         self.certificates.as_ref()
     }
 
-    pub fn diff_with_cached(&self, path: PathBuf) -> Result<bool, Error> {
-        OpenOptions::new()
-            .read(true)
-            .open(path)
-            .map_err(|err| err.context(ErrorKind::Initialize(InitializeErrorReason::LoadSettings)))
-            .and_then(|mut file: FsFile| {
-                let mut buffer = String::new();
-                file.read_to_string(&mut buffer)
-                    .context(ErrorKind::Initialize(InitializeErrorReason::LoadSettings))?;
-                let s = serde_json::to_string(self)
-                    .context(ErrorKind::Initialize(InitializeErrorReason::LoadSettings))?;
-                let s = Sha256::digest_str(&s);
-                let encoded = base64::encode(&s);
-                if encoded == buffer {
-                    debug!("Config state matches supplied config.");
-                    Ok(false)
-                } else {
-                    Ok(true)
-                }
-            })
-            .or_else(|err| {
+    pub fn diff_with_cached(&self, path: &Path) -> bool {
+        fn diff_with_cached_inner<T>(
+            cached_settings: &Settings<T>,
+            path: &Path,
+        ) -> Result<bool, LoadSettingsError>
+        where
+            T: DeserializeOwned + Serialize,
+        {
+            let mut file = OpenOptions::new().read(true).open(path)?;
+            let mut buffer = String::new();
+            file.read_to_string(&mut buffer)?;
+            let s = serde_json::to_string(cached_settings)?;
+            let s = Sha256::digest_str(&s);
+            let encoded = base64::encode(&s);
+            if encoded == buffer {
+                debug!("Config state matches supplied config.");
+                Ok(false)
+            } else {
+                Ok(true)
+            }
+        }
+
+        match diff_with_cached_inner(self, path) {
+            Ok(result) => result,
+
+            Err(err) => {
                 log_failure(Level::Debug, &err);
                 debug!("Error reading config backup.");
-                Ok(true)
-            })
+                true
+            }
+        }
     }
 }
 
+#[derive(Debug, Fail)]
+#[fail(display = "Could not load settings")]
+pub struct LoadSettingsError(#[cause] Context<Box<dyn std::fmt::Display + Send + Sync>>);
+
+impl From<std::io::Error> for LoadSettingsError {
+    fn from(err: std::io::Error) -> Self {
+        LoadSettingsError(Context::new(Box::new(err)))
+    }
+}
+
+impl From<config::ConfigError> for LoadSettingsError {
+    fn from(err: config::ConfigError) -> Self {
+        LoadSettingsError(Context::new(Box::new(err)))
+    }
+}
+
+impl From<serde_json::Error> for LoadSettingsError {
+    fn from(err: serde_json::Error) -> Self {
+        LoadSettingsError(Context::new(Box::new(err)))
+    }
+}
+
+#[derive(Clone, Copy, Debug, Fail)]
+pub enum ParseManualDeviceConnectionStringError {
+    #[fail(
+        display = "The Connection String is empty. Please update the config.yaml and provide the IoTHub connection information."
+    )]
+    Empty,
+
+    #[fail(display = "The Connection String is missing required parameter {}", _0)]
+    MissingRequiredParameter(&'static str),
+
+    #[fail(
+        display = "The Connection String has a malformed value for parameter {}.",
+        _0
+    )]
+    MalformedParameter(&'static str),
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
     use config::{Config, File, FileFormat};
     use edgelet_docker::DockerConfig;
+    use std::fs::File as FsFile;
     use std::io::Write;
     use tempdir::TempDir;
 
@@ -320,19 +466,19 @@ mod tests {
 
     #[test]
     fn no_file_gets_error() {
-        let settings = Settings::<DockerConfig>::new(Some("garbage"));
+        let settings = Settings::<DockerConfig>::new(Some(Path::new("garbage")));
         assert!(settings.is_err());
     }
 
     #[test]
     fn bad_file_gets_error() {
-        let settings = Settings::<DockerConfig>::new(Some(BAD_SETTINGS));
+        let settings = Settings::<DockerConfig>::new(Some(Path::new(BAD_SETTINGS)));
         assert!(settings.is_err());
     }
 
     #[test]
     fn manual_file_gets_sample_connection_string() {
-        let settings = Settings::<DockerConfig>::new(Some(GOOD_SETTINGS));
+        let settings = Settings::<DockerConfig>::new(Some(Path::new(GOOD_SETTINGS)));
         println!("{:?}", settings);
         assert!(settings.is_ok());
         let s = settings.unwrap();
@@ -340,13 +486,13 @@ mod tests {
         let connection_string = unwrap_manual_provisioning(p);
         assert_eq!(
             connection_string,
-            "HostName=something.something.com;DeviceId=something;SharedAccessKey=something"
+            "HostName=something.something.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U="
         );
     }
 
     #[test]
     fn manual_file_gets_sample_tg_paths() {
-        let settings = Settings::<DockerConfig>::new(Some(GOOD_SETTINGS_TG));
+        let settings = Settings::<DockerConfig>::new(Some(Path::new(GOOD_SETTINGS_TG)));
         println!("{:?}", settings);
         assert!(settings.is_ok());
         let s = settings.unwrap();
@@ -365,7 +511,7 @@ mod tests {
 
     #[test]
     fn dps_prov_symmetric_key_get_settings() {
-        let settings = Settings::<DockerConfig>::new(Some(GOOD_SETTINGS_DPS_SYM_KEY));
+        let settings = Settings::<DockerConfig>::new(Some(Path::new(GOOD_SETTINGS_DPS_SYM_KEY)));
         println!("{:?}", settings);
         assert!(settings.is_ok());
         let s = settings.unwrap();
@@ -385,58 +531,53 @@ mod tests {
     fn diff_with_same_cached_returns_false() {
         let tmp_dir = TempDir::new("blah").unwrap();
         let path = tmp_dir.path().join("cache");
-        let settings = Settings::<DockerConfig>::new(Some(GOOD_SETTINGS)).unwrap();
+        let settings = Settings::<DockerConfig>::new(Some(Path::new(GOOD_SETTINGS))).unwrap();
         let settings_to_write = serde_json::to_string(&settings).unwrap();
         let sha_to_write = Sha256::digest_str(&settings_to_write);
         let base64_to_write = base64::encode(&sha_to_write);
-        FsFile::create(path.clone())
+        FsFile::create(&path)
             .unwrap()
             .write_all(base64_to_write.as_bytes())
             .unwrap();
-        assert_eq!(settings.diff_with_cached(path).unwrap(), false);
+        assert_eq!(settings.diff_with_cached(&path), false);
     }
 
     #[test]
     fn diff_with_same_cached_env_var_unordered_returns_false() {
         let tmp_dir = TempDir::new("blah").unwrap();
         let path = tmp_dir.path().join("cache");
-        let settings1 = Settings::<DockerConfig>::new(Some(GOOD_SETTINGS2)).unwrap();
+        let settings1 = Settings::<DockerConfig>::new(Some(Path::new(GOOD_SETTINGS2))).unwrap();
         let settings_to_write = serde_json::to_string(&settings1).unwrap();
         let sha_to_write = Sha256::digest_str(&settings_to_write);
         let base64_to_write = base64::encode(&sha_to_write);
-        FsFile::create(path.clone())
+        FsFile::create(&path)
             .unwrap()
             .write_all(base64_to_write.as_bytes())
             .unwrap();
-        let settings = Settings::<DockerConfig>::new(Some(GOOD_SETTINGS)).unwrap();
-        assert_eq!(settings.diff_with_cached(path).unwrap(), false);
+        let settings = Settings::<DockerConfig>::new(Some(Path::new(GOOD_SETTINGS))).unwrap();
+        assert_eq!(settings.diff_with_cached(&path), false);
     }
 
     #[test]
     fn diff_with_different_cached_returns_true() {
         let tmp_dir = TempDir::new("blah").unwrap();
         let path = tmp_dir.path().join("cache");
-        let settings1 = Settings::<DockerConfig>::new(Some(GOOD_SETTINGS1)).unwrap();
+        let settings1 = Settings::<DockerConfig>::new(Some(Path::new(GOOD_SETTINGS1))).unwrap();
         let settings_to_write = serde_json::to_string(&settings1).unwrap();
         let sha_to_write = Sha256::digest_str(&settings_to_write);
         let base64_to_write = base64::encode(&sha_to_write);
-        FsFile::create(path.clone())
+        FsFile::create(&path)
             .unwrap()
             .write_all(base64_to_write.as_bytes())
             .unwrap();
-        let settings = Settings::<DockerConfig>::new(Some(GOOD_SETTINGS)).unwrap();
-        assert_eq!(settings.diff_with_cached(path).unwrap(), true);
+        let settings = Settings::<DockerConfig>::new(Some(Path::new(GOOD_SETTINGS))).unwrap();
+        assert_eq!(settings.diff_with_cached(&path), true);
     }
 
     #[test]
     fn diff_with_no_file_returns_true() {
-        let settings = Settings::<DockerConfig>::new(Some(GOOD_SETTINGS)).unwrap();
-        assert_eq!(
-            settings
-                .diff_with_cached(PathBuf::from("i dont exist"))
-                .unwrap(),
-            true
-        );
+        let settings = Settings::<DockerConfig>::new(Some(Path::new(GOOD_SETTINGS))).unwrap();
+        assert_eq!(settings.diff_with_cached(Path::new("i dont exist")), true);
     }
 
     #[test]
diff --git a/edgelet/iotedged/test/linux/bad_sample_settings.yaml b/edgelet/edgelet-config/test/linux/bad_sample_settings.yaml
similarity index 100%
rename from edgelet/iotedged/test/linux/bad_sample_settings.yaml
rename to edgelet/edgelet-config/test/linux/bad_sample_settings.yaml
diff --git a/edgelet/iotedged/test/linux/sample_settings.dps.sym.yaml b/edgelet/edgelet-config/test/linux/sample_settings.dps.sym.yaml
similarity index 100%
rename from edgelet/iotedged/test/linux/sample_settings.dps.sym.yaml
rename to edgelet/edgelet-config/test/linux/sample_settings.dps.sym.yaml
diff --git a/edgelet/iotedged/test/linux/sample_settings.tg.yaml b/edgelet/edgelet-config/test/linux/sample_settings.tg.yaml
similarity index 94%
rename from edgelet/iotedged/test/linux/sample_settings.tg.yaml
rename to edgelet/edgelet-config/test/linux/sample_settings.tg.yaml
index 6e35137cc61..bc48a498273 100644
--- a/edgelet/iotedged/test/linux/sample_settings.tg.yaml
+++ b/edgelet/edgelet-config/test/linux/sample_settings.tg.yaml
@@ -2,7 +2,7 @@
 # Configures the provisioning mode
 provisioning:
   source: "manual"
-  device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=something"
+  device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U="
 
 certificates:
   device_ca_cert: "device_ca_cert.pem"
diff --git a/edgelet/iotedged/test/linux/sample_settings.yaml b/edgelet/edgelet-config/test/linux/sample_settings.yaml
similarity index 93%
rename from edgelet/iotedged/test/linux/sample_settings.yaml
rename to edgelet/edgelet-config/test/linux/sample_settings.yaml
index e2ae2d07f46..7dcf7f2ef60 100644
--- a/edgelet/iotedged/test/linux/sample_settings.yaml
+++ b/edgelet/edgelet-config/test/linux/sample_settings.yaml
@@ -2,7 +2,7 @@
 # Configures the provisioning mode
 provisioning:
   source: "manual"
-  device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=something"
+  device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U="
 agent:
   name: "edgeAgent"
   type: "docker"
diff --git a/edgelet/iotedged/test/linux/sample_settings1.yaml b/edgelet/edgelet-config/test/linux/sample_settings1.yaml
similarity index 92%
rename from edgelet/iotedged/test/linux/sample_settings1.yaml
rename to edgelet/edgelet-config/test/linux/sample_settings1.yaml
index c2698274fdd..bbac6df76b1 100644
--- a/edgelet/iotedged/test/linux/sample_settings1.yaml
+++ b/edgelet/edgelet-config/test/linux/sample_settings1.yaml
@@ -2,7 +2,7 @@
 # Configures the provisioning mode
 provisioning:
   source: "manual"
-  device_connection_string: "HostName=something1.something1.com;DeviceId=something;SharedAccessKey=something"
+  device_connection_string: "HostName=something1.something1.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U="
 agent:
   name: "edgeAgent"
   type: "docker"
diff --git a/edgelet/iotedged/test/linux/sample_settings2.yaml b/edgelet/edgelet-config/test/linux/sample_settings2.yaml
similarity index 93%
rename from edgelet/iotedged/test/linux/sample_settings2.yaml
rename to edgelet/edgelet-config/test/linux/sample_settings2.yaml
index 3172340b8bc..24f70b096f1 100644
--- a/edgelet/iotedged/test/linux/sample_settings2.yaml
+++ b/edgelet/edgelet-config/test/linux/sample_settings2.yaml
@@ -2,7 +2,7 @@
 # Configures the provisioning mode
 provisioning:
   source: "manual"
-  device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=something"
+  device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U="
 agent:
   name: "edgeAgent"
   type: "docker"
diff --git a/edgelet/iotedged/test/windows/bad_sample_settings.yaml b/edgelet/edgelet-config/test/windows/bad_sample_settings.yaml
similarity index 91%
rename from edgelet/iotedged/test/windows/bad_sample_settings.yaml
rename to edgelet/edgelet-config/test/windows/bad_sample_settings.yaml
index 3d0ccb3bc28..a0325d3994f 100644
--- a/edgelet/iotedged/test/windows/bad_sample_settings.yaml
+++ b/edgelet/edgelet-config/test/windows/bad_sample_settings.yaml
@@ -17,5 +17,5 @@ listen:
   management_uri: "http://0.0.0.0:8080"
 homedir: "C:\\Temp"
 moby_runtime:
-  uri: "http://localhost:2375"
+  uri: "npipe://./pipe/iotedge_moby_engine"
   network: "azure-iot-edge"
diff --git a/edgelet/iotedged/test/windows/sample_settings.dps.sym.yaml b/edgelet/edgelet-config/test/windows/sample_settings.dps.sym.yaml
similarity index 90%
rename from edgelet/iotedged/test/windows/sample_settings.dps.sym.yaml
rename to edgelet/edgelet-config/test/windows/sample_settings.dps.sym.yaml
index 7c11575c667..08068ccb9c3 100644
--- a/edgelet/iotedged/test/windows/sample_settings.dps.sym.yaml
+++ b/edgelet/edgelet-config/test/windows/sample_settings.dps.sym.yaml
@@ -28,6 +28,7 @@ connect:
 listen:
   workload_uri: "http://0.0.0.0:8081"
   management_uri: "http://0.0.0.0:8080"
-docker_uri: "http://localhost:2375"
 homedir: "C:\\Temp"
-network: "azure-iot-edge"
+moby_runtime:
+  uri: "npipe://./pipe/iotedge_moby_engine"
+  network: "azure-iot-edge"
diff --git a/edgelet/iotedged/test/windows/sample_settings.tg.yaml b/edgelet/edgelet-config/test/windows/sample_settings.tg.yaml
similarity index 85%
rename from edgelet/iotedged/test/windows/sample_settings.tg.yaml
rename to edgelet/edgelet-config/test/windows/sample_settings.tg.yaml
index a364f59199a..96d53be3b1b 100644
--- a/edgelet/iotedged/test/windows/sample_settings.tg.yaml
+++ b/edgelet/edgelet-config/test/windows/sample_settings.tg.yaml
@@ -2,7 +2,7 @@
 # Configures the provisioning mode
 provisioning:
   source: "manual"
-  device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=something"
+  device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U="
 
 certificates:
   device_ca_cert: "device_ca_cert.pem"
@@ -30,6 +30,7 @@ connect:
 listen:
   workload_uri: "http://0.0.0.0:8081"
   management_uri: "http://0.0.0.0:8080"
-docker_uri: "http://localhost:2375"
 homedir: "C:\\Temp"
-network: "azure-iot-edge"
+moby_runtime:
+  uri: "npipe://./pipe/iotedge_moby_engine"
+  network: "azure-iot-edge"
diff --git a/edgelet/edgelet-config/test/windows/sample_settings.yaml b/edgelet/edgelet-config/test/windows/sample_settings.yaml
new file mode 100644
index 00000000000..5c25c3d952a
--- /dev/null
+++ b/edgelet/edgelet-config/test/windows/sample_settings.yaml
@@ -0,0 +1,31 @@
+
+# Configures the provisioning mode
+provisioning:
+  source: "manual"
+  device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U="
+agent:
+  name: "edgeAgent"
+  type: "docker"
+  env:
+    abc: "value1"
+    acd: "value2"
+  config:
+    image: "microsoft/azureiotedge-agent:1.0"
+    auth: {}
+hostname: "localhost"
+
+# Sets the connection uris for clients
+connect:
+  workload_uri: "http://localhost:8081"
+  management_uri: "http://localhost:8080"
+
+# Sets the uris to listen on
+# These can be different than the connect uris.
+# For instance, when using the fd:// scheme for systemd
+listen:
+  workload_uri: "http://0.0.0.0:8081"
+  management_uri: "http://0.0.0.0:8080"
+homedir: "C:\\Temp"
+moby_runtime:
+  uri: "npipe://./pipe/iotedge_moby_engine"
+  network: "azure-iot-edge"
diff --git a/edgelet/iotedged/test/windows/sample_settings1.yaml b/edgelet/edgelet-config/test/windows/sample_settings1.yaml
similarity index 86%
rename from edgelet/iotedged/test/windows/sample_settings1.yaml
rename to edgelet/edgelet-config/test/windows/sample_settings1.yaml
index 3cf62c602df..76071a9bca1 100644
--- a/edgelet/iotedged/test/windows/sample_settings1.yaml
+++ b/edgelet/edgelet-config/test/windows/sample_settings1.yaml
@@ -2,7 +2,7 @@
 # Configures the provisioning mode
 provisioning:
   source: "manual"
-  device_connection_string: "HostName=something1.something1.com;DeviceId=something;SharedAccessKey=something"
+  device_connection_string: "HostName=something1.something1.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U="
 agent:
   name: "edgeAgent"
   type: "docker"
@@ -25,4 +25,4 @@ listen:
   management_uri: "http://0.0.0.0:8080"
 homedir: "C:\\Temp"
 moby_runtime:
-  uri: "http://localhost:2375"
+  uri: "npipe://./pipe/iotedge_moby_engine"
diff --git a/edgelet/iotedged/test/windows/sample_settings2.yaml b/edgelet/edgelet-config/test/windows/sample_settings2.yaml
similarity index 87%
rename from edgelet/iotedged/test/windows/sample_settings2.yaml
rename to edgelet/edgelet-config/test/windows/sample_settings2.yaml
index 7f20f746893..0c848370cb8 100644
--- a/edgelet/iotedged/test/windows/sample_settings2.yaml
+++ b/edgelet/edgelet-config/test/windows/sample_settings2.yaml
@@ -2,7 +2,7 @@
 # Configures the provisioning mode
 provisioning:
   source: "manual"
-  device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=something"
+  device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U="
 agent:
   name: "edgeAgent"
   type: "docker"
@@ -27,5 +27,5 @@ listen:
   management_uri: "http://0.0.0.0:8080"
 homedir: "C:\\Temp"
 moby_runtime:
-  uri: "http://localhost:2375"
+  uri: "npipe://./pipe/iotedge_moby_engine"
   network: "azure-iot-edge"
diff --git a/edgelet/iotedged/test/windows/sample_settings.yaml b/edgelet/edgelet-config/test/windows/sample_settings_notmoby.yaml
similarity index 93%
rename from edgelet/iotedged/test/windows/sample_settings.yaml
rename to edgelet/edgelet-config/test/windows/sample_settings_notmoby.yaml
index d748c107e61..c1b70d30f1d 100644
--- a/edgelet/iotedged/test/windows/sample_settings.yaml
+++ b/edgelet/edgelet-config/test/windows/sample_settings_notmoby.yaml
@@ -2,7 +2,7 @@
 # Configures the provisioning mode
 provisioning:
   source: "manual"
-  device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=something"
+  device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U="
 agent:
   name: "edgeAgent"
   type: "docker"
diff --git a/edgelet/edgelet-core/src/error.rs b/edgelet/edgelet-core/src/error.rs
index 5b8ffa2c2e8..c4e00f36c98 100644
--- a/edgelet/edgelet-core/src/error.rs
+++ b/edgelet/edgelet-core/src/error.rs
@@ -42,6 +42,9 @@ pub enum ErrorKind {
     #[fail(display = "Invalid module type {:?}", _0)]
     InvalidModuleType(String),
 
+    #[fail(display = "Invalid URL {:?}", _0)]
+    InvalidUrl(String),
+
     #[fail(display = "Item not found.")]
     KeyStoreItemNotFound,
 
diff --git a/edgelet/edgelet-core/src/lib.rs b/edgelet/edgelet-core/src/lib.rs
index f0d74e38d64..92611a84d38 100644
--- a/edgelet/edgelet-core/src/lib.rs
+++ b/edgelet/edgelet-core/src/lib.rs
@@ -21,9 +21,15 @@ extern crate serde_derive;
 extern crate serde_json;
 extern crate sha2;
 extern crate tokio;
+extern crate url;
 
 extern crate edgelet_utils;
 
+use std::path::{Path, PathBuf};
+
+use failure::ResultExt;
+use url::Url;
+
 mod authorization;
 mod certificate_properties;
 pub mod crypto;
@@ -54,9 +60,50 @@ lazy_static! {
         .map(|version| option_env!("BUILD_SOURCEVERSION")
             .map(|sha| format!("{} ({})", version, sha))
             .unwrap_or_else(|| version.to_string()))
-        .unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string());
+        .unwrap_or_else(|| include_str!("../../version.txt").trim().to_string());
 }
 
 pub fn version() -> &'static str {
     &VERSION
 }
+
+pub trait UrlExt {
+    fn to_uds_file_path(&self) -> Result<PathBuf, Error>;
+    fn to_base_path(&self) -> Result<PathBuf, Error>;
+}
+
+impl UrlExt for Url {
+    fn to_uds_file_path(&self) -> Result<PathBuf, Error> {
+        debug_assert_eq!(self.scheme(), UNIX_SCHEME);
+
+        if cfg!(windows) {
+            // We get better handling of Windows file syntax if we parse a
+            // unix:// URL as a file:// URL. Specifically:
+            // - On Unix, `Url::parse("unix:///path")?.to_file_path()` succeeds and
+            //   returns "/path".
+            // - On Windows, `Url::parse("unix:///C:/path")?.to_file_path()` fails
+            //   with Err(()).
+            // - On Windows, `Url::parse("file:///C:/path")?.to_file_path()` succeeds
+            //   and returns "C:\\path".
+            debug_assert_eq!(self.scheme(), UNIX_SCHEME);
+            let mut s = self.to_string();
+            s.replace_range(..4, "file");
+            let url = Url::parse(&s).with_context(|_| ErrorKind::InvalidUrl(s.clone()))?;
+            let path = url
+                .to_file_path()
+                .map_err(|()| ErrorKind::InvalidUrl(url.to_string()))?;
+            Ok(path)
+        } else {
+            Ok(Path::new(self.path()).to_path_buf())
+        }
+    }
+
+    fn to_base_path(&self) -> Result<PathBuf, Error> {
+        match self.scheme() {
+            "unix" => Ok(self.to_uds_file_path()?),
+            _ => Ok(self.as_str().into()),
+        }
+    }
+}
+
+pub const UNIX_SCHEME: &str = "unix";
diff --git a/edgelet/edgelet-docker/src/runtime.rs b/edgelet/edgelet-docker/src/runtime.rs
index f74f63b9df4..c136f65122a 100644
--- a/edgelet/edgelet-docker/src/runtime.rs
+++ b/edgelet/edgelet-docker/src/runtime.rs
@@ -21,9 +21,9 @@ use docker::apis::configuration::Configuration;
 use docker::models::{ContainerCreateBody, InlineResponse200, InlineResponse2001, NetworkConfig};
 use edgelet_core::{
     pid::Pid, LogOptions, Module, ModuleRegistry, ModuleRuntime, ModuleRuntimeState, ModuleSpec,
-    ModuleTop, RegistryOperation, RuntimeOperation, SystemInfo as CoreSystemInfo,
+    ModuleTop, RegistryOperation, RuntimeOperation, SystemInfo as CoreSystemInfo, UrlExt,
 };
-use edgelet_http::{UrlConnector, UrlExt};
+use edgelet_http::UrlConnector;
 use edgelet_utils::{ensure_not_empty_with_context, log_failure};
 
 use error::{Error, ErrorKind, Result};
diff --git a/edgelet/edgelet-http-mgmt/src/client/module.rs b/edgelet/edgelet-http-mgmt/src/client/module.rs
index 3c158e81a56..5628aa80e19 100644
--- a/edgelet/edgelet-http-mgmt/src/client/module.rs
+++ b/edgelet/edgelet-http-mgmt/src/client/module.rs
@@ -17,9 +17,9 @@ use serde_json;
 use url::Url;
 
 use edgelet_core::*;
-use edgelet_core::{ModuleOperation, RuntimeOperation, SystemInfo as CoreSystemInfo};
+use edgelet_core::{ModuleOperation, RuntimeOperation, SystemInfo as CoreSystemInfo, UrlExt};
 use edgelet_docker::{self, DockerConfig};
-use edgelet_http::{UrlConnector, UrlExt, API_VERSION};
+use edgelet_http::{UrlConnector, API_VERSION};
 
 use error::{Error, ErrorKind};
 
diff --git a/edgelet/edgelet-http/src/lib.rs b/edgelet/edgelet-http/src/lib.rs
index fe308f28501..03d44b5f1d1 100644
--- a/edgelet/edgelet-http/src/lib.rs
+++ b/edgelet/edgelet-http/src/lib.rs
@@ -65,7 +65,6 @@ use std::net;
 use std::net::ToSocketAddrs;
 #[cfg(unix)]
 use std::os::unix::io::FromRawFd;
-use std::path::{Path, PathBuf};
 use std::sync::Arc;
 
 use failure::{Fail, ResultExt};
@@ -81,6 +80,7 @@ use tokio::net::TcpListener;
 use tokio_uds::UnixListener;
 use url::Url;
 
+use edgelet_core::{UrlExt, UNIX_SCHEME};
 use edgelet_utils::log_failure;
 
 pub mod authorization;
@@ -102,8 +102,9 @@ use self::pid::PidService;
 use self::util::incoming::Incoming;
 
 const HTTP_SCHEME: &str = "http";
+#[cfg(windows)]
+const PIPE_SCHEME: &str = "npipe";
 const TCP_SCHEME: &str = "tcp";
-const UNIX_SCHEME: &str = "unix";
 #[cfg(unix)]
 const FD_SCHEME: &str = "fd";
 
@@ -237,7 +238,9 @@ impl HyperExt for Http {
                 Incoming::Tcp(listener)
             }
             UNIX_SCHEME => {
-                let path = url.to_uds_file_path()?;
+                let path = url
+                    .to_uds_file_path()
+                    .map_err(|_| ErrorKind::InvalidUrl(url.to_string()))?;
                 unix::listener(path)?
             }
             #[cfg(unix)]
@@ -295,42 +298,3 @@ impl HyperExt for Http {
         })
     }
 }
-
-pub trait UrlExt {
-    fn to_uds_file_path(&self) -> Result<PathBuf, Error>;
-    fn to_base_path(&self) -> Result<PathBuf, Error>;
-}
-
-impl UrlExt for Url {
-    fn to_uds_file_path(&self) -> Result<PathBuf, Error> {
-        debug_assert_eq!(self.scheme(), UNIX_SCHEME);
-
-        if cfg!(windows) {
-            // We get better handling of Windows file syntax if we parse a
-            // unix:// URL as a file:// URL. Specifically:
-            // - On Unix, `Url::parse("unix:///path")?.to_file_path()` succeeds and
-            //   returns "/path".
-            // - On Windows, `Url::parse("unix:///C:/path")?.to_file_path()` fails
-            //   with Err(()).
-            // - On Windows, `Url::parse("file:///C:/path")?.to_file_path()` succeeds
-            //   and returns "C:\\path".
-            debug_assert_eq!(self.scheme(), UNIX_SCHEME);
-            let mut s = self.to_string();
-            s.replace_range(..4, "file");
-            let url = Url::parse(&s).with_context(|_| ErrorKind::InvalidUrl(s.clone()))?;
-            let path = url
-                .to_file_path()
-                .map_err(|()| ErrorKind::InvalidUrl(url.to_string()))?;
-            Ok(path)
-        } else {
-            Ok(Path::new(self.path()).to_path_buf())
-        }
-    }
-
-    fn to_base_path(&self) -> Result<PathBuf, Error> {
-        match self.scheme() {
-            "unix" => Ok(self.to_uds_file_path()?),
-            _ => Ok(self.as_str().into()),
-        }
-    }
-}
diff --git a/edgelet/edgelet-http/src/util/connector.rs b/edgelet/edgelet-http/src/util/connector.rs
index d3629e59873..24464cac810 100644
--- a/edgelet/edgelet-http/src/util/connector.rs
+++ b/edgelet/edgelet-http/src/util/connector.rs
@@ -28,14 +28,13 @@ use hyperlocal::{UnixConnector, Uri as HyperlocalUri};
 use hyperlocal_windows::{UnixConnector, Uri as HyperlocalUri};
 use url::{ParseError, Url};
 
+use edgelet_core::UrlExt;
+
 use error::{Error, ErrorKind, InvalidUrlReason};
 use util::{socket_file_exists, StreamSelector};
-use UrlExt;
-
-const UNIX_SCHEME: &str = "unix";
 #[cfg(windows)]
-const PIPE_SCHEME: &str = "npipe";
-const HTTP_SCHEME: &str = "http";
+use PIPE_SCHEME;
+use {HTTP_SCHEME, UNIX_SCHEME};
 
 pub enum UrlConnector {
     Http(HttpConnector),
@@ -51,7 +50,9 @@ impl UrlConnector {
             PIPE_SCHEME => Ok(UrlConnector::Pipe(PipeConnector)),
 
             UNIX_SCHEME => {
-                let file_path = url.to_uds_file_path()?;
+                let file_path = url
+                    .to_uds_file_path()
+                    .map_err(|_| ErrorKind::InvalidUrl(url.to_string()))?;
                 if socket_file_exists(&file_path) {
                     Ok(UrlConnector::Unix(UnixConnector::new()))
                 } else {
diff --git a/edgelet/iotedge-diagnostics/Cargo.toml b/edgelet/iotedge-diagnostics/Cargo.toml
new file mode 100644
index 00000000000..188914a6c06
--- /dev/null
+++ b/edgelet/iotedge-diagnostics/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "iotedge-diagnostics"
+version = "0.1.0"
+authors = ["Azure IoT Edge Devs"]
+publish = false
+edition = "2018"
+
+[dependencies]
+clap = "2.31"
+futures = "0.1"
+hyper = "0.12"
+serde_json = "1.0"
+tokio = "0.1"
+url = "1.7"
+
+edgelet-core = { path = "../edgelet-core" }
+management = { path = "../management" }
+
+[target.'cfg(unix)'.dependencies]
+hyperlocal = "0.6"
+
+[target.'cfg(windows)'.dependencies]
+hyperlocal-windows = { git = "https://github.com/Azure/hyperlocal-windows" }
diff --git a/edgelet/iotedge-diagnostics/docker/linux/amd64/Dockerfile b/edgelet/iotedge-diagnostics/docker/linux/amd64/Dockerfile
new file mode 100644
index 00000000000..b7b376ad334
--- /dev/null
+++ b/edgelet/iotedge-diagnostics/docker/linux/amd64/Dockerfile
@@ -0,0 +1,3 @@
+FROM alpine:3.9
+
+COPY ./docker/linux/amd64/iotedge-diagnostics /iotedge-diagnostics
diff --git a/edgelet/iotedge-diagnostics/docker/linux/arm32v7/Dockerfile b/edgelet/iotedge-diagnostics/docker/linux/arm32v7/Dockerfile
new file mode 100644
index 00000000000..056f0ef8fb2
--- /dev/null
+++ b/edgelet/iotedge-diagnostics/docker/linux/arm32v7/Dockerfile
@@ -0,0 +1,3 @@
+FROM arm32v6/alpine:3.9
+
+COPY ./docker/linux/arm32v7/iotedge-diagnostics /iotedge-diagnostics
diff --git a/edgelet/iotedge-diagnostics/docker/manifest.yaml.template b/edgelet/iotedge-diagnostics/docker/manifest.yaml.template
new file mode 100644
index 00000000000..07132a47bb5
--- /dev/null
+++ b/edgelet/iotedge-diagnostics/docker/manifest.yaml.template
@@ -0,0 +1,18 @@
+image: __REGISTRY__/__NAMESPACE__/azureiotedge-diagnostics:__VERSION__
+tags: __TAGS__
+manifests:
+  -
+    image: __REGISTRY__/__NAMESPACE__/azureiotedge-diagnostics:__VERSION__-linux-amd64
+    platform:
+      architecture: amd64
+      os: linux
+  -
+    image: __REGISTRY__/__NAMESPACE__/azureiotedge-diagnostics:__VERSION__-linux-arm32v7
+    platform:
+      architecture: arm
+      os: linux
+  -
+    image: __REGISTRY__/__NAMESPACE__/azureiotedge-diagnostics:__VERSION__-windows-amd64
+    platform:
+      architecture: amd64
+      os: windows
diff --git a/edgelet/iotedge-diagnostics/docker/windows/amd64/Dockerfile b/edgelet/iotedge-diagnostics/docker/windows/amd64/Dockerfile
new file mode 100644
index 00000000000..75413f5fe3c
--- /dev/null
+++ b/edgelet/iotedge-diagnostics/docker/windows/amd64/Dockerfile
@@ -0,0 +1,3 @@
+FROM mcr.microsoft.com/windows/nanoserver:1809_amd64
+
+COPY ./docker/windows/amd64/iotedge-diagnostics.exe /iotedge-diagnostics
diff --git a/edgelet/iotedge-diagnostics/src/main.rs b/edgelet/iotedge-diagnostics/src/main.rs
new file mode 100644
index 00000000000..c978ebd9f19
--- /dev/null
+++ b/edgelet/iotedge-diagnostics/src/main.rs
@@ -0,0 +1,176 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+#![deny(rust_2018_idioms, warnings)]
+#![deny(clippy::all, clippy::pedantic)]
+#![allow(clippy::use_self)]
+
+#[macro_use]
+extern crate clap;
+
+use std::net::{TcpStream, ToSocketAddrs};
+
+use futures::{Future, Stream};
+
+use edgelet_core::UrlExt;
+
+#[cfg(unix)]
+use hyperlocal::{UnixConnector, Uri as UnixUri};
+#[cfg(windows)]
+use hyperlocal_windows::{UnixConnector, Uri as UnixUri};
+
+fn main() -> Result<(), Error> {
+    let app = app_from_crate!()
+        .setting(clap::AppSettings::SubcommandRequiredElseHelp)
+        .setting(clap::AppSettings::VersionlessSubcommands)
+        .subcommand(
+            clap::SubCommand::with_name("edge-agent")
+                .about("edge-agent diagnostics")
+                .arg(
+                    clap::Arg::with_name("management-uri")
+                        .long("management-uri")
+                        .required(true)
+                        .takes_value(true)
+                        .help("URI of management endpoint"),
+                ),
+        )
+        .subcommand(
+            clap::SubCommand::with_name("iothub")
+                .about("connect to Azure IoT Hub")
+                .arg(
+                    clap::Arg::with_name("hostname")
+                        .long("hostname")
+                        .required(true)
+                        .takes_value(true)
+                        .help("Hostname of Azure IoT Hub"),
+                )
+                .arg(
+                    clap::Arg::with_name("port")
+                        .long("port")
+                        .required(true)
+                        .takes_value(true)
+                        .help("Port to connect to"),
+                ),
+        )
+        .subcommand(clap::SubCommand::with_name("local-time").about("print local time"));
+
+    let matches = app.get_matches();
+
+    let mut runtime = tokio::runtime::Runtime::new()
+        .map_err(|err| format!("could not create tokio runtime: {}", err))?;
+
+    match matches.subcommand() {
+        ("edge-agent", Some(matches)) => {
+            let management_uri = matches
+                .value_of("management-uri")
+                .expect("parameter is required");
+            let management_uri = url::Url::parse(management_uri)
+                .map_err(|err| format!("could not parse management URI: {}", err))?;
+
+            let list_modules_response = match management_uri.scheme() {
+                "unix" => {
+                    let client =
+                        hyper::Client::builder().build::<_, hyper::Body>(UnixConnector::new());
+                    let uri = UnixUri::new(
+                        management_uri
+                            .to_uds_file_path()
+                            .map_err(|err| format!("couldn't get file path from URI: {}", err))?,
+                        "/modules/?api-version=2018-06-28",
+                    );
+                    client.get(uri.into())
+                }
+
+                "http" => {
+                    let client = hyper::Client::new();
+                    let uri = management_uri
+                        .join("/modules/?api-version=2018-06-28")
+                        .map_err(|err| {
+                            format!("could not construct list-modules request URI: {}", err)
+                        })?;
+                    client.get(uri.to_string().parse().map_err(|err| {
+                        format!("could not convert list-modules request URI from url::Url to hyper::Uri: {}", err)
+                    })?)
+                }
+
+                scheme => {
+                    return Err(format!(
+                        "unrecognized scheme {:?} in management URI {:?}",
+                        scheme, management_uri
+                    )
+                    .into());
+                }
+            };
+
+            let f = list_modules_response
+                .then(|response| {
+                    let response = response.map_err(|err| {
+                        format!("could not execute list-modules request: {}", err)
+                    })?;
+                    assert_eq!(
+                        response.status(),
+                        hyper::StatusCode::OK,
+                        "list-modules request did not succeed"
+                    );
+                    Ok::<_, Error>(response.into_body().concat2().map_err(|err| {
+                        format!("could not execute list-modules request: {}", err).into()
+                    }))
+                })
+                .flatten()
+                .and_then(|body| {
+                    let _: management::models::ModuleList = serde_json::from_slice(&*body)
+                        .map_err(|err| format!("could not parse list-modules response: {}", err))?;
+                    Ok::<_, Error>(())
+                });
+
+            runtime.block_on(f)?;
+        }
+
+        ("iothub", Some(matches)) => {
+            let iothub_hostname = matches.value_of("hostname").expect("parameter is required");
+
+            let port = matches.value_of("port").expect("parameter is required");
+
+            let port = port
+                .parse()
+                .map_err(|err| format!("could not parse port: {}", err))?;
+
+            let iothub_host = (iothub_hostname, port)
+                .to_socket_addrs()
+                .map_err(|err| format!("could not resolve Azure IoT Hub hostname: {}", err))?
+                .next()
+                .ok_or_else(|| {
+                    "could not resolve Azure IoT Hub hostname: no addresses found".to_owned()
+                })?;
+
+            let _ = TcpStream::connect_timeout(&iothub_host, std::time::Duration::from_secs(10))
+                .map_err(|err| format!("could not connect to IoT Hub: {}", err))?;
+        }
+
+        ("local-time", _) => {
+            println!(
+                "{}",
+                std::time::SystemTime::now()
+                    .duration_since(std::time::UNIX_EPOCH)
+                    .unwrap()
+                    .as_secs()
+            );
+        }
+
+        (subcommand, _) => panic!("unexpected subcommand {}", subcommand),
+    }
+
+    Ok(())
+}
+
+struct Error(String);
+
+impl std::fmt::Debug for Error {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.0)
+    }
+}
+
+impl From<String> for Error {
+    fn from(s: String) -> Self {
+        Error(s)
+    }
+}
diff --git a/edgelet/iotedge/Cargo.toml b/edgelet/iotedge/Cargo.toml
index 905875e6205..acbd9515499 100644
--- a/edgelet/iotedge/Cargo.toml
+++ b/edgelet/iotedge/Cargo.toml
@@ -7,15 +7,36 @@ The iotedge tool is used to manage the IoT Edge runtime.
 """
 
 [dependencies]
+atty = "0.2"
 bytes = "0.4"
 chrono = "0.4"
 chrono-humanize = "0.0.11"
 clap = "2.31"
 failure = "0.1"
 futures = "0.1"
+hyper = "0.12"
+native-tls = "0.2"
+openssl = "0.10"
+regex = "0.2"
+serde = "1.0"
+serde_derive = "1.0"
+serde_json = "1.0"
 tabwriter = "1.0"
+termcolor = "0.3"
 tokio = "0.1"
 url = "1.7"
 
+docker = { path = "../docker-rs" }
+edgelet-config = { path = "../edgelet-config" }
 edgelet-core = { path = "../edgelet-core" }
+edgelet-docker = { path = "../edgelet-docker" }
+edgelet-http = { path = "../edgelet-http" }
 edgelet-http-mgmt = { path = "../edgelet-http-mgmt" }
+management = { path = "../management" }
+mini-sntp = { path = "../mini-sntp" }
+
+[target.'cfg(unix)'.dependencies]
+libc = "0.2"
+
+[target.'cfg(windows)'.dependencies]
+winapi = { version = "0.3", features = ["winsock2"] }
diff --git a/edgelet/iotedge/src/check.rs b/edgelet/iotedge/src/check.rs
new file mode 100644
index 00000000000..f5a796d3fb4
--- /dev/null
+++ b/edgelet/iotedge/src/check.rs
@@ -0,0 +1,1723 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+use std;
+use std::borrow::Cow;
+use std::ffi::{CStr, OsStr, OsString};
+use std::fs::File;
+use std::io::{Read, Write};
+use std::net::TcpStream;
+use std::path::PathBuf;
+use std::process::Command;
+
+use failure::Fail;
+use failure::{self, Context, ResultExt};
+use futures::future::{self, FutureResult};
+use futures::{Future, IntoFuture, Stream};
+#[cfg(unix)]
+use libc;
+use regex::Regex;
+use serde_json;
+use termcolor::WriteColor;
+
+use edgelet_config::{Provisioning, Settings};
+use edgelet_core::UrlExt;
+use edgelet_docker::DockerConfig;
+use edgelet_http::client::ClientImpl;
+use edgelet_http::MaybeProxyClient;
+
+use error::{Error, ErrorKind, FetchLatestVersionsReason};
+use LatestVersions;
+
+pub struct Check {
+    config_file: PathBuf,
+    container_engine_config_path: PathBuf,
+    diagnostics_image_name: String,
+    iotedged: PathBuf,
+    latest_versions: Result<super::LatestVersions, Option<Error>>,
+    ntp_server: String,
+    verbose: bool,
+
+    // These optional fields are populated by the pre-checks
+    settings: Option<Settings<DockerConfig>>,
+    docker_host_arg: Option<String>,
+    docker_server_version: Option<String>,
+    iothub_hostname: Option<String>,
+}
+
+/// The various ways a check can resolve.
+///
+/// Check functions return `Result<CheckResult, failure::Error>` where `Err` represents the check failed.
+#[derive(Debug)]
+enum CheckResult {
+    /// Check succeeded.
+    Ok,
+
+    /// Check failed with a warning.
+    Warning(failure::Error),
+
+    /// Check is not applicable and was ignored. Should be treated as success.
+    Ignored,
+
+    /// Check was skipped because of errors from some previous checks. Should be treated as an error.
+    Skipped,
+
+    /// Check failed, and further checks should not be performed.
+    Fatal(failure::Error),
+}
+
+impl Check {
+    pub fn new(
+        config_file: PathBuf,
+        container_engine_config_path: PathBuf,
+        diagnostics_image_name: String,
+        expected_iotedged_version: Option<String>,
+        iotedged: PathBuf,
+        ntp_server: String,
+        verbose: bool,
+    ) -> impl Future<Item = Self, Error = Error> + Send {
+        let latest_versions = if let Some(expected_iotedged_version) = expected_iotedged_version {
+            future::Either::A(future::ok::<_, Error>(LatestVersions {
+                iotedged: expected_iotedged_version,
+            }))
+        } else {
+            let proxy = std::env::var("HTTPS_PROXY")
+                .ok()
+                .or_else(|| std::env::var("https_proxy").ok());
+            let proxy = if let Some(proxy) = proxy {
+                let proxy = proxy
+                    .parse::<hyper::Uri>()
+                    .context(ErrorKind::FetchLatestVersions(
+                        FetchLatestVersionsReason::CreateClient,
+                    ));
+                match proxy {
+                    Ok(proxy) => future::ok(Some(proxy)),
+                    Err(err) => future::err(Error::from(err)),
+                }
+            } else {
+                future::ok(None)
+            };
+
+            let hyper_client = proxy.and_then(|proxy| {
+                Ok(
+                    MaybeProxyClient::new(proxy).context(ErrorKind::FetchLatestVersions(
+                        FetchLatestVersionsReason::CreateClient,
+                    ))?,
+                )
+            });
+
+            let request = hyper::Request::get("https://aka.ms/latest-iotedge-stable")
+                .body(hyper::Body::default())
+                .expect("can't fail to create request");
+
+            future::Either::B(
+                hyper_client
+                    .and_then(|hyper_client| {
+                        hyper_client.call(request).then(|response| {
+                            let response = response.context(ErrorKind::FetchLatestVersions(
+                                FetchLatestVersionsReason::GetResponse,
+                            ))?;
+                            Ok((response, hyper_client))
+                        })
+                    })
+                    .and_then(move |(response, hyper_client)| match response.status() {
+                        hyper::StatusCode::MOVED_PERMANENTLY => {
+                            let uri = response
+                                .headers()
+                                .get(hyper::header::LOCATION)
+                                .ok_or_else(|| {
+                                    ErrorKind::FetchLatestVersions(
+                                        FetchLatestVersionsReason::InvalidOrMissingLocationHeader,
+                                    )
+                                })?
+                                .to_str()
+                                .context(ErrorKind::FetchLatestVersions(
+                                    FetchLatestVersionsReason::InvalidOrMissingLocationHeader,
+                                ))?;
+                            let request = hyper::Request::get(uri)
+                                .body(hyper::Body::default())
+                                .expect("can't fail to create request");
+                            Ok(hyper_client.call(request).map_err(|err| {
+                                err.context(ErrorKind::FetchLatestVersions(
+                                    FetchLatestVersionsReason::GetResponse,
+                                ))
+                                .into()
+                            }))
+                        }
+                        status_code => Err(ErrorKind::FetchLatestVersions(
+                            FetchLatestVersionsReason::ResponseStatusCode(status_code),
+                        )
+                        .into()),
+                    })
+                    .flatten()
+                    .and_then(|response| -> Result<_, Error> {
+                        match response.status() {
+                            hyper::StatusCode::OK => {
+                                Ok(response.into_body().concat2().map_err(|err| {
+                                    err.context(ErrorKind::FetchLatestVersions(
+                                        FetchLatestVersionsReason::GetResponse,
+                                    ))
+                                    .into()
+                                }))
+                            }
+                            status_code => Err(ErrorKind::FetchLatestVersions(
+                                FetchLatestVersionsReason::ResponseStatusCode(status_code),
+                            )
+                            .into()),
+                        }
+                    })
+                    .flatten()
+                    .and_then(|body| {
+                        Ok(serde_json::from_slice(&body).context(
+                            ErrorKind::FetchLatestVersions(FetchLatestVersionsReason::GetResponse),
+                        )?)
+                    }),
+            )
+        };
+
+        latest_versions.then(move |latest_versions| {
+            Ok(Check {
+                config_file,
+                container_engine_config_path,
+                diagnostics_image_name,
+                iotedged,
+                ntp_server,
+                latest_versions: latest_versions.map_err(Some),
+                verbose,
+
+                settings: None,
+                docker_host_arg: None,
+                docker_server_version: None,
+                iothub_hostname: None,
+            })
+        })
+    }
+
+    fn execute_inner(&mut self) -> Result<(), Error> {
+        const CHECKS: &[(
+            &str, // Section name
+            &[(
+                &str,                                                  // Check description
+                fn(&mut Check) -> Result<CheckResult, failure::Error>, // Check function
+            )],
+        )] = &[
+            (
+                "Configuration checks",
+                &[
+                    ("config.yaml is well-formed", parse_settings),
+                    (
+                        "config.yaml has well-formed connection string",
+                        settings_connection_string,
+                    ),
+                    (
+                        "container engine is installed and functional",
+                        container_engine,
+                    ),
+                    ("config.yaml has correct hostname", settings_hostname),
+                    (
+                        "config.yaml has correct URIs for daemon mgmt endpoint",
+                        daemon_mgmt_endpoint_uri,
+                    ),
+                    ("latest security daemon", iotedged_version),
+                    ("host time is close to real time", host_local_time),
+                    ("container time is close to host time", container_local_time),
+                    ("DNS server", container_engine_dns),
+                    ("production readiness: certificates", settings_certificates),
+                    (
+                        "production readiness: certificates expiry",
+                        settings_certificates_expiry,
+                    ),
+                    (
+                        "production readiness: container engine",
+                        settings_moby_runtime_uri,
+                    ),
+                    (
+                        "production readiness: logs policy",
+                        container_engine_logrotate,
+                    ),
+                ],
+            ),
+            (
+                "Connectivity checks",
+                &[
+                    (
+                        "host can connect to and perform TLS handshake with IoT Hub AMQP port",
+                        |check| connection_to_iot_hub_host(check, 5671),
+                    ),
+                    (
+                        "host can connect to and perform TLS handshake with IoT Hub HTTPS port",
+                        |check| connection_to_iot_hub_host(check, 443),
+                    ),
+                    (
+                        "host can connect to and perform TLS handshake with IoT Hub MQTT port",
+                        |check| connection_to_iot_hub_host(check, 8883),
+                    ),
+                    ("container on the default network can connect to IoT Hub AMQP port", |check| {
+                        if cfg!(windows) {
+                            // The default network is the same as the IoT Edge module network,
+                            // so let the module network checks handle it.
+                            Ok(CheckResult::Ignored)
+                        } else {
+                            connection_to_iot_hub_container(check, 5671, false)
+                        }
+                    }),
+                    ("container on the default network can connect to IoT Hub HTTPS port", |check| {
+                        if cfg!(windows) {
+                            // The default network is the same as the IoT Edge module network,
+                            // so let the module network checks handle it.
+                            Ok(CheckResult::Ignored)
+                        } else {
+                            connection_to_iot_hub_container(check, 443, false)
+                        }
+                    }),
+                    ("container on the default network can connect to IoT Hub MQTT port", |check| {
+                        if cfg!(windows) {
+                            // The default network is the same as the IoT Edge module network,
+                            // so let the module network checks handle it.
+                            Ok(CheckResult::Ignored)
+                        } else {
+                            connection_to_iot_hub_container(check, 8883, false)
+                        }
+                    }),
+                    ("container on the IoT Edge module network can connect to IoT Hub AMQP port", |check| {
+                        connection_to_iot_hub_container(check, 5671, true)
+                    }),
+                    ("container on the IoT Edge module network can connect to IoT Hub HTTPS port", |check| {
+                        connection_to_iot_hub_container(check, 443, true)
+                    }),
+                    ("container on the IoT Edge module network can connect to IoT Hub MQTT port", |check| {
+                        connection_to_iot_hub_container(check, 8883, true)
+                    }),
+                    ("Edge Hub can bind to ports on host", edge_hub_ports_on_host),
+                ],
+            ),
+        ];
+
+        let mut stdout = termcolor::StandardStream::stdout(termcolor::ColorChoice::Auto);
+        let success_color_spec = {
+            let mut success_color_spec = termcolor::ColorSpec::new();
+            if cfg!(windows) {
+                // `Color::Green` maps to `FG_GREEN` which is too hard to read on the default blue-background profile that PS uses.
+                // PS uses `FG_GREEN | FG_INTENSITY` == 8 == `[ConsoleColor]::Green` as the foreground color for its error text,
+                // so mimic that.
+                success_color_spec.set_fg(Some(termcolor::Color::Rgb(0, 255, 0)));
+            } else {
+                success_color_spec.set_fg(Some(termcolor::Color::Green));
+            }
+            success_color_spec
+        };
+        let warning_color_spec = {
+            let mut warning_color_spec = termcolor::ColorSpec::new();
+            if cfg!(windows) {
+                // `Color::Yellow` maps to `FOREGROUND_GREEN | FOREGROUND_RED` == 6 == `ConsoleColor::DarkYellow`.
+                // In its default blue-background profile, PS uses `ConsoleColor::DarkYellow` as its default foreground text color
+                // and maps it to a dark gray.
+                //
+                // So use explicit RGB to define yellow for Windows. Also use a black background to mimic PS warnings.
+                //
+                // Ref:
+                // - https://docs.rs/termcolor/0.3.6/src/termcolor/lib.rs.html#1380 defines `termcolor::Color::Yellow` as `wincolor::Color::Yellow`
+                // - https://docs.rs/wincolor/0.1.6/x86_64-pc-windows-msvc/src/wincolor/win.rs.html#18
+                //   defines `wincolor::Color::Yellow` as `FG_YELLOW`, which in turn is `FOREGROUND_GREEN | FOREGROUND_RED`
+                // - https://docs.microsoft.com/en-us/windows/console/char-info-str defines `FOREGROUND_GREEN | FOREGROUND_RED` as `2 | 4 == 6`
+                // - https://docs.microsoft.com/en-us/dotnet/api/system.consolecolor#fields defines `6` as `[ConsoleColor]::DarkYellow`
+                // - `$Host.UI.RawUI.ForegroundColor` in the default PS profile is `DarkYellow`, and writing in it prints dark gray text.
+                warning_color_spec.set_fg(Some(termcolor::Color::Rgb(255, 255, 0)));
+                warning_color_spec.set_bg(Some(termcolor::Color::Black));
+            } else {
+                warning_color_spec.set_fg(Some(termcolor::Color::Yellow));
+            }
+            warning_color_spec
+        };
+        let error_color_spec = {
+            let mut error_color_spec = termcolor::ColorSpec::new();
+            if cfg!(windows) {
+                // `Color::Red` maps to `FG_RED` which is too hard to read on the default blue-background profile that PS uses.
+                // PS uses `FG_RED | FG_INTENSITY` == 12 == `[ConsoleColor]::Red` as the foreground color for its error text,
+                // with black background, so mimic that.
+                error_color_spec.set_fg(Some(termcolor::Color::Rgb(255, 0, 0)));
+                error_color_spec.set_bg(Some(termcolor::Color::Black));
+            } else {
+                error_color_spec.set_fg(Some(termcolor::Color::Red));
+            }
+            error_color_spec
+        };
+        let is_a_tty = atty::is(atty::Stream::Stdout);
+
+        let mut have_warnings = false;
+        let mut have_skipped = false;
+        let mut have_fatal = false;
+        let mut have_errors = false;
+
+        for (section_name, section_checks) in CHECKS {
+            if have_fatal {
+                break;
+            }
+
+            println!("{}", section_name);
+            println!("{}", "-".repeat(section_name.len()));
+
+            for (check_name, check) in *section_checks {
+                if have_fatal {
+                    break;
+                }
+
+                match check(self) {
+                    Ok(CheckResult::Ok) => {
+                        colored(&mut stdout, &success_color_spec, is_a_tty, |stdout| {
+                            writeln!(stdout, "\u{221a} {}", check_name)?;
+                            Ok(())
+                        });
+                    }
+
+                    Ok(CheckResult::Warning(warning)) => {
+                        have_warnings = true;
+
+                        colored(&mut stdout, &warning_color_spec, is_a_tty, |stdout| {
+                            writeln!(stdout, "\u{203c} {}", check_name)?;
+
+                            let message = warning.to_string();
+
+                            write_lines(stdout, "    ", "    ", message.lines())?;
+
+                            if self.verbose {
+                                for cause in warning.iter_causes() {
+                                    write_lines(
+                                        stdout,
+                                        "        caused by: ",
+                                        "                   ",
+                                        cause.to_string().lines(),
+                                    )?;
+                                }
+                            }
+
+                            Ok(())
+                        });
+                    }
+
+                    Ok(CheckResult::Ignored) => (),
+
+                    Ok(CheckResult::Skipped) => {
+                        have_skipped = true;
+
+                        if self.verbose {
+                            colored(&mut stdout, &warning_color_spec, is_a_tty, |stdout| {
+                                writeln!(stdout, "\u{203c} {}", check_name)?;
+                                writeln!(stdout, "    skipping because of previous failures")?;
+                                Ok(())
+                            });
+                        }
+                    }
+
+                    Ok(CheckResult::Fatal(err)) => {
+                        have_fatal = true;
+
+                        colored(&mut stdout, &error_color_spec, is_a_tty, |stdout| {
+                            writeln!(stdout, "\u{00d7} {}", check_name)?;
+
+                            let message = err.to_string();
+
+                            write_lines(stdout, "    ", "    ", message.lines())?;
+
+                            if self.verbose {
+                                for cause in err.iter_causes() {
+                                    write_lines(
+                                        stdout,
+                                        "        caused by: ",
+                                        "                   ",
+                                        cause.to_string().lines(),
+                                    )?;
+                                }
+                            }
+
+                            Ok(())
+                        });
+                    }
+
+                    Err(err) => {
+                        have_errors = true;
+
+                        colored(&mut stdout, &error_color_spec, is_a_tty, |stdout| {
+                            writeln!(stdout, "\u{00d7} {}", check_name)?;
+
+                            let message = err.to_string();
+
+                            write_lines(stdout, "    ", "    ", message.lines())?;
+
+                            if self.verbose {
+                                for cause in err.iter_causes() {
+                                    write_lines(
+                                        stdout,
+                                        "        caused by: ",
+                                        "                   ",
+                                        cause.to_string().lines(),
+                                    )?;
+                                }
+                            }
+
+                            Ok(())
+                        });
+                    }
+                }
+            }
+
+            println!();
+        }
+
+        match (have_warnings, have_skipped, have_fatal || have_errors) {
+            (false, false, false) => {
+                colored(&mut stdout, &success_color_spec, is_a_tty, |stdout| {
+                    writeln!(stdout, "All checks succeeded.")?;
+                    Ok(())
+                });
+
+                Ok(())
+            }
+
+            (_, _, true) => {
+                colored(&mut stdout, &error_color_spec, is_a_tty, |stdout| {
+                    write!(stdout, "One or more checks raised errors.")?;
+                    if self.verbose {
+                        writeln!(stdout)?;
+                    } else {
+                        writeln!(stdout, " Re-run with --verbose for more details.")?;
+                    }
+                    Ok(())
+                });
+
+                Err(ErrorKind::Diagnostics.into())
+            }
+
+            (_, true, _) => {
+                colored(&mut stdout, &warning_color_spec, is_a_tty, |stdout| {
+                    write!(
+                        stdout,
+                        "One or more checks were skipped due to errors from other checks."
+                    )?;
+                    if self.verbose {
+                        writeln!(stdout)?;
+                    } else {
+                        writeln!(stdout, " Re-run with --verbose for more details.")?;
+                    }
+                    Ok(())
+                });
+
+                Ok(())
+            }
+
+            (true, _, _) => {
+                colored(&mut stdout, &warning_color_spec, is_a_tty, |stdout| {
+                    write!(stdout, "One or more checks raised warnings.")?;
+                    if self.verbose {
+                        writeln!(stdout)?;
+                    } else {
+                        writeln!(stdout, " Re-run with --verbose for more details.")?;
+                    }
+                    Ok(())
+                });
+
+                Ok(())
+            }
+        }
+    }
+}
+
+impl ::Command for Check {
+    type Future = FutureResult<(), Error>;
+
+    fn execute(&mut self) -> Self::Future {
+        self.execute_inner().into_future()
+    }
+}
+
+fn parse_settings(check: &mut Check) -> Result<CheckResult, failure::Error> {
+    let config_file = &check.config_file;
+
+    // The config crate just returns a "file not found" error when it can't open the file for any reason,
+    // even if the real error was a permissions issue.
+    //
+    // So we first try to open the file for reading ourselves.
+    if let Err(err) = File::open(config_file) {
+        if err.kind() == std::io::ErrorKind::PermissionDenied {
+            return Ok(CheckResult::Fatal(
+                err.context(format!(
+                    "Could not open file {}. You might need to run this command as {}.",
+                    config_file.display(),
+                    if cfg!(windows) {
+                        "Administrator"
+                    } else {
+                        "root"
+                    },
+                ))
+                .into(),
+            ));
+        } else {
+            return Err(err
+                .context(format!("Could not open file {}", config_file.display()))
+                .into());
+        }
+    }
+
+    let settings = match Settings::new(Some(config_file)) {
+        Ok(settings) => settings,
+        Err(err) => {
+            let message = if check.verbose {
+                format!(
+                    "The IoT Edge daemon's configuration file {} is not well-formed.\n\
+                     Note: In case of syntax errors, the error may not be exactly at the reported line number and position.",
+                    config_file.display(),
+                )
+            } else {
+                format!(
+                    "The IoT Edge daemon's configuration file {} is not well-formed.",
+                    config_file.display(),
+                )
+            };
+            return Err(err.context(message).into());
+        }
+    };
+
+    check.settings = Some(settings);
+
+    Ok(CheckResult::Ok)
+}
+
+fn settings_connection_string(check: &mut Check) -> Result<CheckResult, failure::Error> {
+    let settings = if let Some(settings) = &check.settings {
+        settings
+    } else {
+        return Ok(CheckResult::Skipped);
+    };
+
+    if let Provisioning::Manual(manual) = settings.provisioning() {
+        let (_, _, hub) = manual.parse_device_connection_string().context(
+            "Invalid connection string format detected.\n\
+             Please check the value of the provisioning.device_connection_string parameter.",
+        )?;
+        check.iothub_hostname = Some(hub.to_owned());
+    }
+
+    Ok(CheckResult::Ok)
+}
+
+fn container_engine(check: &mut Check) -> Result<CheckResult, failure::Error> {
+    let settings = if let Some(settings) = &check.settings {
+        settings
+    } else {
+        return Ok(CheckResult::Skipped);
+    };
+
+    let uri = settings.moby_runtime().uri();
+
+    let docker_host_arg = match uri.scheme() {
+        "unix" => uri.to_string(),
+
+        "npipe" => {
+            let mut uri = uri.to_string();
+            uri.replace_range(0.."npipe://".len(), "npipe:////");
+            uri
+        }
+
+        scheme => {
+            return Err(Context::new(format!(
+                "Could not communicate with container engine at {}. The scheme {} is invalid.",
+                uri, scheme,
+            ))
+            .into());
+        }
+    };
+
+    let output = docker(
+        &docker_host_arg,
+        &["version", "--format", "{{.Server.Version}}"],
+    );
+    let output = match output {
+        Ok(output) => output,
+        Err((message, err)) => {
+            let mut error_message = format!(
+                "Could not communicate with container engine at {}.\n\
+                 Please check your moby-engine installation and ensure the service is running.",
+                uri,
+            );
+
+            if let Some(message) = message {
+                #[cfg(unix)]
+                {
+                    if message.contains("Got permission denied") {
+                        error_message += "\nYou might need to run this command as root.";
+                        return Ok(CheckResult::Fatal(err.context(error_message).into()));
+                    }
+                }
+
+                #[cfg(windows)]
+                {
+                    if message.contains("Access is denied") {
+                        error_message += "\nYou might need to run this command as Administrator.";
+                        return Ok(CheckResult::Fatal(err.context(error_message).into()));
+                    }
+                }
+            }
+
+            return Err(err.context(error_message).into());
+        }
+    };
+
+    check.docker_host_arg = Some(docker_host_arg);
+
+    check.docker_server_version = Some(String::from_utf8_lossy(&output).into_owned());
+
+    Ok(CheckResult::Ok)
+}
+
+fn settings_hostname(check: &mut Check) -> Result<CheckResult, failure::Error> {
+    let settings = if let Some(settings) = &check.settings {
+        settings
+    } else {
+        return Ok(CheckResult::Skipped);
+    };
+
+    let config_hostname = settings.hostname();
+
+    let machine_hostname = unsafe {
+        let mut result = vec![0_u8; 256];
+
+        #[cfg(unix)]
+        {
+            if libc::gethostname(result.as_mut_ptr() as _, result.len()) != 0 {
+                return Err(
+                    std::io::Error::last_os_error() // Calls errno
+                        .context("Could not get hostname: gethostname failed")
+                        .into(),
+                );
+            }
+        }
+
+        #[cfg(windows)]
+        #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
+        {
+            // libstd only calls WSAStartup when something under std::net gets used, like creating a TcpStream.
+            // Since we haven't done anything like that up to this point, it ends up not being called.
+            // So we call it manually.
+            //
+            // The process is going to exit anyway, so there's no reason to make the effort of
+            // calling the corresponding WSACleanup later.
+            let mut wsa_data: winapi::um::winsock2::WSADATA = std::mem::zeroed();
+            match winapi::um::winsock2::WSAStartup(0x202, &mut wsa_data) {
+                0 => (),
+                result => {
+                    return Err(Context::new(format!(
+                        "Could not get hostname: WSAStartup failed with {}",
+                        result,
+                    ))
+                    .into());
+                }
+            }
+
+            if winapi::um::winsock2::gethostname(result.as_mut_ptr() as _, result.len() as _) != 0 {
+                // Can't use std::io::Error::last_os_error() because that calls GetLastError, not WSAGetLastError
+                let winsock_err = winapi::um::winsock2::WSAGetLastError();
+
+                return Err(Context::new(format!(
+                    "Could not get hostname: gethostname failed with {}",
+                    winsock_err,
+                ))
+                .into());
+            }
+        }
+
+        let nul_index = result.iter().position(|&b| b == b'\0').ok_or_else(|| {
+            Context::new("Could not get hostname: gethostname did not return NUL-terminated string")
+        })?;
+
+        CStr::from_bytes_with_nul_unchecked(&result[..=nul_index])
+            .to_str()
+            .context("Could not get hostname: gethostname returned non-ASCII string")?
+            .to_owned()
+    };
+
+    if config_hostname != machine_hostname {
+        return Err(Context::new(format!(
+            "config.yaml has hostname {} but device reports hostname {}",
+            config_hostname, machine_hostname,
+        ))
+        .into());
+    }
+
+    Ok(CheckResult::Ok)
+}
+
+fn daemon_mgmt_endpoint_uri(check: &mut Check) -> Result<CheckResult, failure::Error> {
+    let settings = if let Some(settings) = &check.settings {
+        settings
+    } else {
+        return Ok(CheckResult::Skipped);
+    };
+
+    let docker_host_arg = if let Some(docker_host_arg) = &check.docker_host_arg {
+        docker_host_arg
+    } else {
+        return Ok(CheckResult::Skipped);
+    };
+
+    let connect_management_uri = settings.connect().management_uri();
+    let listen_management_uri = settings.listen().management_uri();
+
+    let mut args: Vec<Cow<'_, OsStr>> = vec![
+        Cow::Borrowed(OsStr::new("run")),
+        Cow::Borrowed(OsStr::new("--rm")),
+    ];
+
+    for (name, value) in settings.agent().env() {
+        args.push(Cow::Borrowed(OsStr::new("-e")));
+        args.push(Cow::Owned(format!("{}={}", name, value).into()));
+    }
+
+    match (connect_management_uri.scheme(), listen_management_uri.scheme()) {
+        ("http", "http") => (),
+
+        ("unix", "unix") | ("unix", "fd") => {
+            args.push(Cow::Borrowed(OsStr::new("-v")));
+
+            let socket_path =
+                connect_management_uri.to_uds_file_path()
+                .context("Could not parse connect.management_uri: does not represent a valid file path")?;
+
+            // On Windows we mount the parent folder because we can't mount the socket files directly
+            #[cfg(windows)]
+            let socket_path =
+                socket_path.parent()
+                .ok_or_else(|| Context::new("Could not parse connect.management_uri: does not have a parent directory"))?;
+
+            let socket_path =
+                socket_path.to_str()
+                .ok_or_else(|| Context::new("Could not parse connect.management_uri: file path is not valid utf-8"))?;
+
+            args.push(Cow::Owned(format!("{}:{}", socket_path, socket_path).into()));
+        },
+
+        (scheme1, scheme2) if scheme1 != scheme2 => return Err(Context::new(
+            format!(
+                "config.yaml has invalid combination of schemes for connect.management_uri ({:?}) and listen.management_uri ({:?})",
+                scheme1, scheme2,
+            ))
+            .into()),
+
+        (scheme, _) => return Err(Context::new(
+            format!("Could not parse connect.management_uri: scheme {} is invalid", scheme),
+        ).into()),
+    }
+
+    args.extend(vec![
+        Cow::Borrowed(OsStr::new(&check.diagnostics_image_name)),
+        Cow::Borrowed(OsStr::new("/iotedge-diagnostics")),
+        Cow::Borrowed(OsStr::new("edge-agent")),
+        Cow::Borrowed(OsStr::new("--management-uri")),
+        Cow::Owned(OsString::from(connect_management_uri.to_string())),
+    ]);
+
+    match docker(docker_host_arg, args) {
+        Ok(_) => Ok(CheckResult::Ok),
+        Err((Some(stderr), err)) => Err(err.context(stderr).into()),
+        Err((None, err)) => Err(err.context("Could not spawn docker process").into()),
+    }
+}
+
+fn iotedged_version(check: &mut Check) -> Result<CheckResult, failure::Error> {
+    let latest_versions = match &mut check.latest_versions {
+        Ok(latest_versions) => &*latest_versions,
+        Err(err) => match err.take() {
+            Some(err) => return Ok(CheckResult::Warning(err.into())),
+            None => return Ok(CheckResult::Skipped),
+        },
+    };
+
+    let mut process = Command::new(&check.iotedged);
+    process.arg("--version");
+
+    if cfg!(windows) {
+        process.env("IOTEDGE_RUN_AS_CONSOLE", "true");
+    }
+
+    let output = process
+        .output()
+        .context("Could not spawn iotedged process")?;
+    if !output.status.success() {
+        return Err(Context::new(format!(
+            "iotedged returned {}, stderr = {}",
+            output.status,
+            String::from_utf8_lossy(&*output.stderr),
+        ))
+        .context("Could not spawn iotedged process")
+        .into());
+    }
+
+    let output =
+        String::from_utf8(output.stdout).context("Could not parse output of iotedged --version")?;
+
+    let iotedged_version_regex = Regex::new(r"^iotedged ([^ ]+)(?: \(.*\))?$")
+        .expect("This hard-coded regex is expected to be valid.");
+    let captures = iotedged_version_regex
+        .captures(output.trim())
+        .ok_or_else(|| {
+            Context::new(format!(
+                "output {:?} does not match expected format",
+                output,
+            ))
+            .context("Could not parse output of iotedged --version")
+        })?;
+    let version = captures
+        .get(1)
+        .expect("unreachable: regex defines one capturing group")
+        .as_str();
+
+    if version != latest_versions.iotedged {
+        return Ok(CheckResult::Warning(
+            Context::new(format!(
+                "Installed IoT Edge daemon has version {} but version {} is available.\n\
+                 Please see https://aka.ms/iotedge-update-runtime for update instructions.",
+                version, latest_versions.iotedged,
+            ))
+            .into(),
+        ));
+    }
+
+    Ok(CheckResult::Ok)
+}
+
+fn host_local_time(check: &mut Check) -> Result<CheckResult, failure::Error> {
+    fn is_server_unreachable_error(err: &mini_sntp::Error) -> bool {
+        match err.kind() {
+            mini_sntp::ErrorKind::ResolveNtpPoolHostname(_) => true,
+            mini_sntp::ErrorKind::SendClientRequest(err)
+            | mini_sntp::ErrorKind::ReceiveServerResponse(err) => {
+                err.kind() == std::io::ErrorKind::TimedOut || // Windows
+                err.kind() == std::io::ErrorKind::WouldBlock // Unix
+            }
+            _ => false,
+        }
+    }
+
+    let mini_sntp::SntpTimeQueryResult {
+        local_clock_offset, ..
+    } = match mini_sntp::query(&check.ntp_server) {
+        Ok(result) => result,
+        Err(err) => {
+            if is_server_unreachable_error(&err) {
+                return Ok(CheckResult::Warning(
+                    err.context("Could not query NTP server").into(),
+                ));
+            } else {
+                return Err(err.context("Could not query NTP server").into());
+            }
+        }
+    };
+
+    if local_clock_offset.num_seconds().abs() >= 10 {
+        return Ok(CheckResult::Warning(Context::new(format!(
+            "Time on the device is out of sync with the NTP server. This may cause problems connecting to IoT Hub.\n\
+             Please ensure time on device is accurate, for example by {}.",
+            if cfg!(windows) {
+                "setting up the Windows Time service to automatically sync with a time server"
+            } else {
+                "installing an NTP daemon"
+            },
+        )).into()));
+    }
+
+    Ok(CheckResult::Ok)
+}
+
+fn container_local_time(check: &mut Check) -> Result<CheckResult, failure::Error> {
+    let docker_host_arg = if let Some(docker_host_arg) = &check.docker_host_arg {
+        docker_host_arg
+    } else {
+        return Ok(CheckResult::Skipped);
+    };
+
+    let expected_duration = std::time::SystemTime::now()
+        .duration_since(std::time::UNIX_EPOCH)
+        .context("Could not query local time of host")?;
+
+    let output = docker(
+        docker_host_arg,
+        vec![
+            "run",
+            "--rm",
+            &check.diagnostics_image_name,
+            "/iotedge-diagnostics",
+            "local-time",
+        ],
+    )
+    .map_err(|(_, err)| err)
+    .context("Could not query local time inside container")?;
+    let output = std::str::from_utf8(&output)
+        .map_err(failure::Error::from)
+        .and_then(|output| output.trim_end().parse::<u64>().map_err(Into::into))
+        .context("Could not parse container output")?;
+    let actual_duration = std::time::Duration::from_secs(output);
+
+    let diff = std::cmp::max(actual_duration, expected_duration)
+        - std::cmp::min(actual_duration, expected_duration);
+    if diff.as_secs() >= 10 {
+        return Err(Context::new("Detected time drift between host and container").into());
+    }
+
+    Ok(CheckResult::Ok)
+}
+
+fn container_engine_dns(check: &mut Check) -> Result<CheckResult, failure::Error> {
+    const MESSAGE: &str =
+        "Container engine is not configured with DNS server setting, which may impact connectivity to IoT Hub.\n\
+         Please see https://aka.ms/iotedge-prod-checklist-dns for best practices.\n\
+         You can ignore this warning if you are setting DNS server per module in the Edge deployment.";
+
+    #[derive(serde_derive::Deserialize)]
+    struct DaemonConfig {
+        dns: Option<Vec<String>>,
+    }
+
+    let daemon_config_file = File::open(&check.container_engine_config_path)
+        .with_context(|_| {
+            format!(
+                "Could not open container engine config file {}",
+                check.container_engine_config_path.display(),
+            )
+        })
+        .context(MESSAGE);
+    let daemon_config_file = match daemon_config_file {
+        Ok(daemon_config_file) => daemon_config_file,
+        Err(err) => {
+            return Ok(CheckResult::Warning(err.into()));
+        }
+    };
+    let daemon_config: DaemonConfig = serde_json::from_reader(daemon_config_file)
+        .with_context(|_| {
+            format!(
+                "Could not parse container engine config file {}",
+                check.container_engine_config_path.display(),
+            )
+        })
+        .context(MESSAGE)?;
+
+    if let Some(&[]) | None = daemon_config.dns.as_ref().map(std::ops::Deref::deref) {
+        return Ok(CheckResult::Warning(Context::new(MESSAGE).into()));
+    }
+
+    Ok(CheckResult::Ok)
+}
+
+fn settings_certificates(check: &mut Check) -> Result<CheckResult, failure::Error> {
+    let settings = if let Some(settings) = &check.settings {
+        settings
+    } else {
+        return Ok(CheckResult::Skipped);
+    };
+
+    if settings.certificates().is_none() {
+        return Ok(CheckResult::Warning(
+            Context::new(
+                "Device is using self-signed, automatically generated certs.\n\
+                 Please see https://aka.ms/iotedge-prod-checklist-certs for best practices.",
+            )
+            .into(),
+        ));
+    }
+
+    Ok(CheckResult::Ok)
+}
+
+fn settings_certificates_expiry(check: &mut Check) -> Result<CheckResult, failure::Error> {
+    fn parse_openssl_time(
+        time: &openssl::asn1::Asn1TimeRef,
+    ) -> chrono::ParseResult<chrono::DateTime<chrono::Utc>> {
+        // openssl::asn1::Asn1TimeRef does not expose any way to convert the ASN1_TIME to a Rust-friendly type
+        //
+        // Its Display impl uses ASN1_TIME_print, so we convert it into a String and parse it back
+        // into a chrono::DateTime<chrono::Utc>
+        let time = time.to_string();
+        let time = chrono::NaiveDateTime::parse_from_str(&time, "%b %e %H:%M:%S %Y GMT")?;
+        Ok(chrono::DateTime::<chrono::Utc>::from_utc(time, chrono::Utc))
+    }
+
+    let settings = if let Some(settings) = &check.settings {
+        settings
+    } else {
+        return Ok(CheckResult::Skipped);
+    };
+
+    let (device_ca_cert_path, device_ca_cert_path_source) = if let Some(certificates) =
+        settings.certificates()
+    {
+        (
+            certificates.device_ca_cert().to_owned(),
+            Cow::Borrowed("certificates.device_ca_cert"),
+        )
+    } else {
+        let certs_dir = settings.homedir().join("hsm").join("certs");
+
+        let mut device_ca_cert_path = None;
+
+        let entries = std::fs::read_dir(&certs_dir)
+            .with_context(|_| format!("Could not enumerate files under {}", certs_dir.display()))?;
+        for entry in entries {
+            let entry = entry.with_context(|_| {
+                format!("Could not enumerate files under {}", certs_dir.display())
+            })?;
+            let path = entry.path();
+            let is_device_ca_cert =
+                if let Some(file_name) = path.file_name().and_then(OsStr::to_str) {
+                    file_name.starts_with("device_ca_alias") && file_name.ends_with(".cert.pem")
+                } else {
+                    false
+                };
+            if is_device_ca_cert {
+                device_ca_cert_path = Some(path);
+                break;
+            }
+        }
+
+        let device_ca_cert_path = device_ca_cert_path.ok_or_else(|| {
+            Context::new(format!(
+                "Could not find device CA certificate under {}",
+                certs_dir.display(),
+            ))
+        })?;
+        let device_ca_cert_path_source = device_ca_cert_path.to_string_lossy().into_owned();
+        (device_ca_cert_path, Cow::Owned(device_ca_cert_path_source))
+    };
+
+    let (not_after, not_before) = File::open(device_ca_cert_path)
+        .map_err(failure::Error::from)
+        .and_then(|mut device_ca_cert_file| {
+            let mut device_ca_cert = vec![];
+            device_ca_cert_file.read_to_end(&mut device_ca_cert)?;
+            let device_ca_cert = openssl::x509::X509::stack_from_pem(&device_ca_cert)?;
+            let device_ca_cert = &device_ca_cert[0];
+
+            let not_after = parse_openssl_time(device_ca_cert.not_after())?;
+            let not_before = parse_openssl_time(device_ca_cert.not_before())?;
+
+            Ok((not_after, not_before))
+        })
+        .with_context(|_| {
+            format!(
+                "Could not parse {} as a valid certificate file",
+                device_ca_cert_path_source,
+            )
+        })?;
+
+    let now = chrono::Utc::now();
+
+    if not_before > now {
+        return Err(Context::new(format!(
+            "Device CA certificate in {} has not-before time {} which is in the future",
+            device_ca_cert_path_source, not_before,
+        ))
+        .into());
+    }
+
+    if not_after < now {
+        return Err(Context::new(format!(
+            "Device CA certificate in {} expired at {}",
+            device_ca_cert_path_source, not_after,
+        ))
+        .into());
+    }
+
+    if not_after < now + chrono::Duration::days(7) {
+        return Ok(CheckResult::Warning(
+            Context::new(format!(
+                "Device CA certificate in {} will expire soon ({})",
+                device_ca_cert_path_source, not_after,
+            ))
+            .into(),
+        ));
+    }
+
+    Ok(CheckResult::Ok)
+}
+
+fn settings_moby_runtime_uri(check: &mut Check) -> Result<CheckResult, failure::Error> {
+    const MESSAGE: &str =
+        "Device is not using a production-supported container engine (moby-engine).\n\
+         Please see https://aka.ms/iotedge-prod-checklist-moby for details.";
+
+    let settings = if let Some(settings) = &check.settings {
+        settings
+    } else {
+        return Ok(CheckResult::Skipped);
+    };
+
+    let docker_server_version = if let Some(docker_server_version) = &check.docker_server_version {
+        docker_server_version
+    } else {
+        return Ok(CheckResult::Skipped);
+    };
+
+    if cfg!(windows) {
+        let moby_runtime_uri = settings.moby_runtime().uri().to_string();
+
+        if moby_runtime_uri != "npipe://./pipe/iotedge_moby_engine" {
+            return Ok(CheckResult::Warning(Context::new(MESSAGE).into()));
+        }
+    }
+
+    let docker_server_major_version = docker_server_version
+        .split('.')
+        .next()
+        .map(std::str::FromStr::from_str);
+    let docker_server_major_version: u32 = match docker_server_major_version {
+        Some(Ok(docker_server_major_version)) => docker_server_major_version,
+        Some(Err(_)) | None => {
+            return Ok(CheckResult::Warning(
+                Context::new(format!(
+                    "Container engine returned malformed version string {:?}",
+                    docker_server_version,
+                ))
+                .context(MESSAGE)
+                .into(),
+            ));
+        }
+    };
+
+    // Moby does not identify itself in any unique way. Moby devs recommend assuming that anything less than version 10 is Moby,
+    // since it's currently 3.x and regular Docker is in the high 10s.
+    if docker_server_major_version >= 10 {
+        return Ok(CheckResult::Warning(Context::new(MESSAGE).into()));
+    }
+
+    Ok(CheckResult::Ok)
+}
+
+fn container_engine_logrotate(check: &mut Check) -> Result<CheckResult, failure::Error> {
+    const MESSAGE: &str =
+        "Container engine is not configured to rotate module logs which may cause it run out of disk space.\n\
+         Please see https://aka.ms/iotedge-prod-checklist-logs for best practices.\n\
+         You can ignore this warning if you are setting log policy per module in the Edge deployment.";
+
+    #[derive(serde_derive::Deserialize)]
+    struct DaemonConfig {
+        #[serde(rename = "log-driver")]
+        log_driver: Option<String>,
+
+        #[serde(rename = "log-opts")]
+        log_opts: Option<DaemonConfigLogOpts>,
+    }
+
+    #[derive(serde_derive::Deserialize)]
+    struct DaemonConfigLogOpts {
+        #[serde(rename = "max-file")]
+        max_file: Option<String>,
+
+        #[serde(rename = "max-size")]
+        max_size: Option<String>,
+    }
+
+    let daemon_config_file = File::open(&check.container_engine_config_path)
+        .with_context(|_| {
+            format!(
+                "Could not open container engine config file {}",
+                check.container_engine_config_path.display(),
+            )
+        })
+        .context(MESSAGE);
+    let daemon_config_file = match daemon_config_file {
+        Ok(daemon_config_file) => daemon_config_file,
+        Err(err) => {
+            return Ok(CheckResult::Warning(err.into()));
+        }
+    };
+    let daemon_config: DaemonConfig = serde_json::from_reader(daemon_config_file)
+        .with_context(|_| {
+            format!(
+                "Could not parse container engine config file {}",
+                check.container_engine_config_path.display(),
+            )
+        })
+        .context(MESSAGE)?;
+
+    if daemon_config.log_driver.is_none() {
+        return Ok(CheckResult::Warning(Context::new(MESSAGE).into()));
+    }
+
+    if let Some(log_opts) = &daemon_config.log_opts {
+        if log_opts.max_file.is_none() {
+            return Ok(CheckResult::Warning(Context::new(MESSAGE).into()));
+        }
+
+        if log_opts.max_size.is_none() {
+            return Ok(CheckResult::Warning(Context::new(MESSAGE).into()));
+        }
+    } else {
+        return Ok(CheckResult::Warning(Context::new(MESSAGE).into()));
+    }
+
+    Ok(CheckResult::Ok)
+}
+
+fn connection_to_iot_hub_host(check: &mut Check, port: u16) -> Result<CheckResult, failure::Error> {
+    let iothub_hostname = if let Some(iothub_hostname) = &check.iothub_hostname {
+        iothub_hostname
+    } else {
+        return Ok(CheckResult::Skipped);
+    };
+
+    let iothub_host = std::net::ToSocketAddrs::to_socket_addrs(&(&**iothub_hostname, port))
+        .with_context(|_| {
+            format!(
+                "Could not connect to {}:{} : could not resolve hostname",
+                iothub_hostname, port,
+            )
+        })?
+        .next()
+        .ok_or_else(|| {
+            Context::new(format!(
+                "Could not connect to {}:{} : could not resolve hostname: no addresses found",
+                iothub_hostname, port,
+            ))
+        })?;
+
+    let stream = TcpStream::connect_timeout(&iothub_host, std::time::Duration::from_secs(10))
+        .with_context(|_| format!("Could not connect to {}:{}", iothub_hostname, port))?;
+
+    let tls_connector = native_tls::TlsConnector::new().with_context(|_| {
+        format!(
+            "Could not connect to {}:{} : could not create TLS connector",
+            iothub_hostname, port,
+        )
+    })?;
+
+    let _ = tls_connector
+        .connect(iothub_hostname, stream)
+        .with_context(|_| {
+            format!(
+                "Could not connect to {}:{} : could not complete TLS handshake",
+                iothub_hostname, port,
+            )
+        })?;
+
+    Ok(CheckResult::Ok)
+}
+
+fn connection_to_iot_hub_container(
+    check: &mut Check,
+    port: u16,
+    use_container_runtime_network: bool,
+) -> Result<CheckResult, failure::Error> {
+    let settings = if let Some(settings) = &check.settings {
+        settings
+    } else {
+        return Ok(CheckResult::Skipped);
+    };
+
+    let docker_host_arg = if let Some(docker_host_arg) = &check.docker_host_arg {
+        docker_host_arg
+    } else {
+        return Ok(CheckResult::Skipped);
+    };
+
+    let iothub_hostname = if let Some(iothub_hostname) = &check.iothub_hostname {
+        iothub_hostname
+    } else {
+        return Ok(CheckResult::Skipped);
+    };
+
+    let network_name = settings.moby_runtime().network();
+
+    let port = port.to_string();
+
+    let mut args = vec!["run", "--rm"];
+
+    if use_container_runtime_network {
+        args.extend(&["--network", network_name]);
+    }
+
+    args.extend(&[
+        &check.diagnostics_image_name,
+        "/iotedge-diagnostics",
+        "iothub",
+        "--hostname",
+        iothub_hostname,
+        "--port",
+        &port,
+    ]);
+
+    if let Err((_, err)) = docker(docker_host_arg, args) {
+        return Err(err
+            .context(format!(
+                "Container on the {} network could not connect to {}:{}",
+                if use_container_runtime_network {
+                    network_name
+                } else {
+                    "default"
+                },
+                iothub_hostname,
+                port,
+            ))
+            .into());
+    }
+
+    Ok(CheckResult::Ok)
+}
+
+fn edge_hub_ports_on_host(check: &mut Check) -> Result<CheckResult, failure::Error> {
+    let docker_host_arg = if let Some(docker_host_arg) = &check.docker_host_arg {
+        docker_host_arg
+    } else {
+        return Ok(CheckResult::Skipped);
+    };
+
+    let inspect_result = docker(docker_host_arg, vec!["inspect", "edgeHub"])
+        .map_err(|(_, err)| err)
+        .and_then(|output| {
+            let (inspect_result,): (docker::models::InlineResponse200,) =
+                serde_json::from_slice(&output)
+                    .context("could not parse result of docker inspect")?;
+            Ok(inspect_result)
+        })
+        .context("Could not check current state of Edge Hub container")?;
+
+    let is_running = inspect_result
+        .state()
+        .and_then(docker::models::InlineResponse200State::running)
+        .cloned()
+        .ok_or_else(|| {
+            Context::new(
+                "Could not check current state of Edge Hub container: \
+                 could not parse result of docker inspect: state.status is not set",
+            )
+        })?;
+    if is_running {
+        // Whatever ports it wanted to bind to must've been available for it to be running
+        return Ok(CheckResult::Ok);
+    }
+
+    let port_bindings = inspect_result
+        .host_config()
+        .and_then(docker::models::HostConfig::port_bindings)
+        .ok_or_else(|| {
+            Context::new(
+                "Could not check port bindings of Edge Hub container: \
+                 could not parse result of docker inspect: host_config.port_bindings is not set",
+            )
+        })?
+        .values()
+        .flatten()
+        .filter_map(docker::models::HostConfigPortBindings::host_port);
+
+    for port_binding in port_bindings {
+        // Try to bind to the port ourselves. If it fails with AddrInUse, then something else has bound to it.
+        match std::net::TcpListener::bind(format!("127.0.0.1:{}", port_binding)) {
+            Ok(_) => (),
+
+            Err(ref err) if err.kind() == std::io::ErrorKind::AddrInUse => {
+                return Err(Context::new(format!(
+                    "Edge hub cannot start on device because port {} is already in use.\n\
+                     Please stop the application using the port or remove the port binding from Edge hub's deployment.",
+                    port_binding,
+                )).into());
+            }
+
+            #[cfg(unix)]
+            Err(ref err) if err.kind() == std::io::ErrorKind::PermissionDenied => {
+                return Ok(CheckResult::Fatal(Context::new(format!(
+                    "Permission denied when attempting to bind to port {}. You might need to run this command as root.",
+                    port_binding,
+                )).into()));
+            }
+
+            Err(err) => {
+                return Err(err
+                    .context(format!(
+                        "Could not check if port {} is available for Edge Hub to bind to",
+                        port_binding,
+                    ))
+                    .into());
+            }
+        }
+    }
+
+    Ok(CheckResult::Ok)
+}
+
+fn colored<F>(
+    stdout: &mut termcolor::StandardStream,
+    spec: &termcolor::ColorSpec,
+    is_a_tty: bool,
+    f: F,
+) where
+    F: FnOnce(&mut termcolor::StandardStream) -> std::io::Result<()>,
+{
+    if is_a_tty {
+        let _ = stdout.set_color(spec);
+    }
+
+    f(stdout).expect("could not write to stdout");
+
+    if is_a_tty {
+        let _ = stdout.reset();
+    }
+}
+
+fn docker<I>(docker_host_arg: &str, args: I) -> Result<Vec<u8>, (Option<String>, failure::Error)>
+where
+    I: IntoIterator,
+    <I as IntoIterator>::Item: AsRef<OsStr>,
+{
+    let mut process = Command::new("docker");
+    process.arg("-H");
+    process.arg(docker_host_arg);
+
+    process.args(args);
+
+    let output = process.output().map_err(|err| {
+        (
+            None,
+            err.context(format!("could not run {:?}", process)).into(),
+        )
+    })?;
+
+    if !output.status.success() {
+        let stderr = String::from_utf8_lossy(&*output.stderr).into_owned();
+        let err = Context::new(format!(
+            "docker returned {}, stderr = {}",
+            output.status, stderr,
+        ))
+        .into();
+        return Err((Some(stderr), err));
+    }
+
+    Ok(output.stdout)
+}
+
+fn write_lines<'a>(
+    writer: &mut impl Write,
+    first_line_indent: &str,
+    other_lines_indent: &str,
+    mut lines: impl Iterator<Item = &'a str>,
+) -> std::io::Result<()> {
+    if let Some(line) = lines.next() {
+        writeln!(writer, "{}{}", first_line_indent, line)?;
+    }
+
+    for line in lines {
+        writeln!(writer, "{}{}", other_lines_indent, line)?;
+    }
+
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn config_file_checks_ok() {
+        let mut runtime = tokio::runtime::current_thread::Runtime::new().unwrap();
+
+        for filename in &[
+            "sample_settings.yaml",
+            "sample_settings.dps.sym.yaml",
+            "sample_settings.tg.yaml",
+        ] {
+            let config_file = format!(
+                "{}/../edgelet-config/test/{}/{}",
+                env!("CARGO_MANIFEST_DIR"),
+                if cfg!(windows) { "windows" } else { "linux" },
+                filename,
+            );
+
+            let mut check = runtime
+                .block_on(super::Check::new(
+                    config_file.into(),
+                    "daemon.json".into(), // unused for this test
+                    "mcr.microsoft.com/azureiotedge-diagnostics:1.0.0".to_owned(), // unused for this test
+                    Some("1.0.0".to_owned()),      // unused for this test
+                    "iotedged".into(),             // unused for this test
+                    "pool.ntp.org:123".to_owned(), // unused for this test
+                    false,
+                ))
+                .unwrap();
+
+            match super::parse_settings(&mut check) {
+                Ok(super::CheckResult::Ok) => (),
+                check_result => panic!("parsing {} returned {:?}", filename, check_result),
+            }
+
+            match super::settings_connection_string(&mut check) {
+                Ok(super::CheckResult::Ok) => (),
+                check_result => panic!(
+                    "checking connection string in {} returned {:?}",
+                    filename, check_result
+                ),
+            }
+
+            match super::settings_hostname(&mut check) {
+                Err(err) => {
+                    let message = err.to_string();
+                    assert!(
+                        message
+                            .starts_with("config.yaml has hostname localhost but device reports"),
+                        "checking hostname in {} produced unexpected error: {}",
+                        filename,
+                        message,
+                    );
+                }
+                check_result => panic!(
+                    "checking hostname in {} returned {:?}",
+                    filename, check_result
+                ),
+            }
+
+            // Pretend it's Moby
+            check.docker_server_version = Some("3.0.3".to_owned());
+
+            match super::settings_moby_runtime_uri(&mut check) {
+                Ok(super::CheckResult::Ok) => (),
+                check_result => panic!(
+                    "checking moby_runtime.uri in {} returned {:?}",
+                    filename, check_result
+                ),
+            }
+        }
+    }
+
+    #[test]
+    fn parse_settings_err() {
+        let mut runtime = tokio::runtime::current_thread::Runtime::new().unwrap();
+
+        let filename = "bad_sample_settings.yaml";
+        let config_file = format!(
+            "{}/../edgelet-config/test/{}/{}",
+            env!("CARGO_MANIFEST_DIR"),
+            if cfg!(windows) { "windows" } else { "linux" },
+            filename,
+        );
+
+        let mut check = runtime
+            .block_on(super::Check::new(
+                config_file.into(),
+                "daemon.json".into(), // unused for this test
+                "mcr.microsoft.com/azureiotedge-diagnostics:1.0.0".to_owned(), // unused for this test
+                Some("1.0.0".to_owned()),      // unused for this test
+                "iotedged".into(),             // unused for this test
+                "pool.ntp.org:123".to_owned(), // unused for this test
+                false,
+            ))
+            .unwrap();
+
+        match super::parse_settings(&mut check) {
+            Err(err) => {
+                let err = err
+                    .iter_causes()
+                    .nth(1)
+                    .expect("expected to find cause-of-cause-of-error");
+                assert!(
+                    err.to_string()
+                        .contains("while parsing a flow mapping, did not find expected ',' or '}' at line 10 column 5"),
+                    "parsing {} produced unexpected error: {}",
+                    filename,
+                    err,
+                );
+            }
+
+            check_result => panic!("parsing {} returned {:?}", filename, check_result),
+        }
+    }
+
+    #[test]
+    #[cfg(windows)]
+    fn moby_runtime_uri_windows_wants_moby_based_on_runtime_uri() {
+        let mut runtime = tokio::runtime::current_thread::Runtime::new().unwrap();
+
+        let filename = "sample_settings_notmoby.yaml";
+        let config_file = format!(
+            "{}/../edgelet-config/test/{}/{}",
+            env!("CARGO_MANIFEST_DIR"),
+            if cfg!(windows) { "windows" } else { "linux" },
+            filename,
+        );
+
+        let mut check = runtime
+            .block_on(super::Check::new(
+                config_file.into(),
+                "daemon.json".into(), // unused for this test
+                "mcr.microsoft.com/azureiotedge-diagnostics:1.0.0".to_owned(), // unused for this test
+                Some("1.0.0".to_owned()),      // unused for this test
+                "iotedged".into(),             // unused for this test
+                "pool.ntp.org:123".to_owned(), // unused for this test
+                false,
+            ))
+            .unwrap();
+
+        match super::parse_settings(&mut check) {
+            Ok(super::CheckResult::Ok) => (),
+            check_result => panic!("parsing {} returned {:?}", filename, check_result),
+        }
+
+        // Pretend it's Moby even though named pipe indicates otherwise
+        check.docker_server_version = Some("3.0.3".to_owned());
+
+        match super::settings_moby_runtime_uri(&mut check) {
+            Ok(super::CheckResult::Warning(warning)) => assert!(
+                warning.to_string().contains(
+                    "Device is not using a production-supported container engine (moby-engine)."
+                ),
+                "checking moby_runtime.uri in {} failed with an unexpected warning: {}",
+                filename,
+                warning
+            ),
+
+            check_result => panic!(
+                "checking moby_runtime.uri in {} returned {:?}",
+                filename, check_result
+            ),
+        }
+    }
+
+    #[test]
+    fn moby_runtime_uri_wants_moby_based_on_server_version() {
+        let mut runtime = tokio::runtime::current_thread::Runtime::new().unwrap();
+
+        let filename = "sample_settings.yaml";
+        let config_file = format!(
+            "{}/../edgelet-config/test/{}/{}",
+            env!("CARGO_MANIFEST_DIR"),
+            if cfg!(windows) { "windows" } else { "linux" },
+            filename,
+        );
+
+        let mut check = runtime
+            .block_on(super::Check::new(
+                config_file.into(),
+                "daemon.json".into(), // unused for this test
+                "mcr.microsoft.com/azureiotedge-diagnostics:1.0.0".to_owned(), // unused for this test
+                Some("1.0.0".to_owned()),      // unused for this test
+                "iotedged".into(),             // unused for this test
+                "pool.ntp.org:123".to_owned(), // unused for this test
+                false,
+            ))
+            .unwrap();
+
+        match super::parse_settings(&mut check) {
+            Ok(super::CheckResult::Ok) => (),
+            check_result => panic!("parsing {} returned {:?}", filename, check_result),
+        }
+
+        // Pretend it's Docker
+        check.docker_server_version = Some("18.09.1".to_owned());
+
+        match super::settings_moby_runtime_uri(&mut check) {
+            Ok(super::CheckResult::Warning(warning)) => assert!(
+                warning.to_string().contains(
+                    "Device is not using a production-supported container engine (moby-engine)."
+                ),
+                "checking moby_runtime.uri in {} failed with an unexpected warning: {}",
+                filename,
+                warning
+            ),
+
+            check_result => panic!(
+                "checking moby_runtime.uri in {} returned {:?}",
+                filename, check_result
+            ),
+        }
+    }
+}
diff --git a/edgelet/iotedge/src/error.rs b/edgelet/iotedge/src/error.rs
index f14c376ad19..55061d3c375 100644
--- a/edgelet/iotedge/src/error.rs
+++ b/edgelet/iotedge/src/error.rs
@@ -10,11 +10,20 @@ pub struct Error {
     inner: Context<ErrorKind>,
 }
 
-#[derive(Clone, Copy, Debug, Fail)]
+#[derive(Clone, Debug, Fail)]
 pub enum ErrorKind {
     #[fail(display = "Invalid value for --host parameter")]
     BadHostParameter,
 
+    #[fail(display = "")]
+    Diagnostics,
+
+    #[fail(
+        display = "Error while fetching latest versions of edge components: {}",
+        _0
+    )]
+    FetchLatestVersions(FetchLatestVersionsReason),
+
     #[fail(display = "Missing --host parameter")]
     MissingHostParameter,
 
@@ -57,3 +66,27 @@ impl From<Context<ErrorKind>> for Error {
         Error { inner }
     }
 }
+
+#[derive(Clone, Copy, Debug)]
+pub enum FetchLatestVersionsReason {
+    CreateClient,
+    GetResponse,
+    InvalidOrMissingLocationHeader,
+    ResponseStatusCode(hyper::StatusCode),
+}
+
+impl Display for FetchLatestVersionsReason {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            FetchLatestVersionsReason::CreateClient => write!(f, "could not create HTTP client"),
+            FetchLatestVersionsReason::GetResponse => write!(f, "could not send HTTP request"),
+            FetchLatestVersionsReason::InvalidOrMissingLocationHeader => write!(
+                f,
+                "redirect response has invalid or missing location header"
+            ),
+            FetchLatestVersionsReason::ResponseStatusCode(status_code) => {
+                write!(f, "response failed with status code {}", status_code)
+            }
+        }
+    }
+}
diff --git a/edgelet/iotedge/src/lib.rs b/edgelet/iotedge/src/lib.rs
index bc256178a59..c6bebdb0ef3 100644
--- a/edgelet/iotedge/src/lib.rs
+++ b/edgelet/iotedge/src/lib.rs
@@ -2,22 +2,38 @@
 
 #![deny(unused_extern_crates, warnings)]
 #![deny(clippy::all, clippy::pedantic)]
-#![allow(clippy::module_name_repetitions, clippy::use_self)]
+#![allow(
+    clippy::module_name_repetitions,
+    clippy::type_complexity,
+    clippy::use_self
+)]
 
 extern crate bytes;
 extern crate chrono;
 extern crate chrono_humanize;
 #[macro_use]
 extern crate clap;
-extern crate edgelet_core;
 extern crate failure;
 #[macro_use]
 extern crate futures;
+#[cfg(unix)]
+extern crate libc;
+extern crate regex;
+#[macro_use]
+extern crate serde_derive;
+extern crate serde_json;
 extern crate tabwriter;
+extern crate termcolor;
 extern crate tokio;
 
+extern crate edgelet_config;
+extern crate edgelet_core;
+extern crate edgelet_docker;
+extern crate edgelet_http;
+
 use futures::Future;
 
+mod check;
 mod error;
 mod list;
 mod logs;
@@ -25,7 +41,8 @@ mod restart;
 mod unknown;
 mod version;
 
-pub use error::{Error, ErrorKind};
+pub use check::Check;
+pub use error::{Error, ErrorKind, FetchLatestVersionsReason};
 pub use list::List;
 pub use logs::Logs;
 pub use restart::Restart;
@@ -33,7 +50,12 @@ pub use unknown::Unknown;
 pub use version::Version;
 
 pub trait Command {
-    type Future: Future<Item = (), Error = Error> + Send;
+    type Future: Future<Item = ()> + Send;
 
     fn execute(&mut self) -> Self::Future;
 }
+
+#[derive(Debug, Deserialize)]
+pub struct LatestVersions {
+    pub iotedged: String,
+}
diff --git a/edgelet/iotedge/src/main.rs b/edgelet/iotedge/src/main.rs
index b95a191502e..e1ab5fcfa34 100644
--- a/edgelet/iotedge/src/main.rs
+++ b/edgelet/iotedge/src/main.rs
@@ -2,22 +2,24 @@
 
 #![deny(unused_extern_crates, warnings)]
 #![deny(clippy::all, clippy::pedantic)]
+#![allow(clippy::similar_names)]
 
 #[macro_use]
 extern crate clap;
 extern crate edgelet_core;
 extern crate edgelet_http_mgmt;
 extern crate failure;
+extern crate futures;
 extern crate iotedge;
 extern crate tokio;
 extern crate url;
 
 use std::io;
-use std::io::Write;
 use std::process;
 
 use clap::{App, AppSettings, Arg, SubCommand};
 use failure::{Fail, ResultExt};
+use futures::Future;
 use url::Url;
 
 use edgelet_core::{LogOptions, LogTail};
@@ -30,24 +32,39 @@ const MGMT_URI: &str = "unix:///var/run/iotedge/mgmt.sock";
 #[cfg(windows)]
 const MGMT_URI: &str = "unix:///C:/ProgramData/iotedge/mgmt/sock";
 
+#[cfg(unix)]
+const DEFAULT_CONFIG_PATH: &str = "/etc/iotedge/config.yaml";
+#[cfg(windows)]
+const DEFAULT_CONFIG_PATH: &str = r"C:\ProgramData\iotedge\config.yaml";
+
+#[cfg(unix)]
+const DEFAULT_CONTAINER_ENGINE_CONFIG_PATH: &str = "/etc/docker/daemon.json";
+#[cfg(windows)]
+const DEFAULT_CONTAINER_ENGINE_CONFIG_PATH: &str =
+    r"C:\ProgramData\iotedge-moby\config\daemon.json";
+
 fn main() {
     if let Err(ref error) = run() {
-        let stderr = &mut io::stderr();
-        let errmsg = "Error writing to stderr";
-
         let mut fail: &Fail = error;
-        writeln!(stderr, "{}", error.to_string()).unwrap_or_else(|_| panic!(errmsg));
-        while let Some(cause) = fail.cause() {
-            writeln!(stderr, "\tcaused by: {}", cause.to_string())
-                .unwrap_or_else(|_| panic!(errmsg));
-            fail = cause;
+
+        eprintln!("{}", error.to_string());
+
+        for cause in fail.iter_causes() {
+            eprintln!("\tcaused by: {}", cause);
         }
+
+        eprintln!();
+
         process::exit(1);
     }
 }
 
 fn run() -> Result<(), Error> {
     let default_uri = option_env!("IOTEDGE_HOST").unwrap_or(MGMT_URI);
+    let default_diagnostics_image_name = format!(
+        "mcr.microsoft.com/azureiotedge-diagnostics:{}",
+        edgelet_core::version()
+    );
 
     let matches = App::new(crate_name!())
         .version(edgelet_core::version())
@@ -64,6 +81,67 @@ fn run() -> Result<(), Error> {
                 .env("IOTEDGE_HOST")
                 .default_value(default_uri),
         )
+        .subcommand(
+            SubCommand::with_name("check")
+                .about("Check for common config and deployment issues")
+                .arg(
+                    Arg::with_name("config-file")
+                        .short("c")
+                        .long("config-file")
+                        .value_name("FILE")
+                        .help("Sets daemon configuration file")
+                        .takes_value(true)
+                        .default_value(DEFAULT_CONFIG_PATH),
+                )
+                .arg(
+                    Arg::with_name("container-engine-config-file")
+                        .long("container-engine-config-file")
+                        .value_name("FILE")
+                        .help("Sets the path of the container engine configuration file")
+                        .takes_value(true)
+                        .default_value(DEFAULT_CONTAINER_ENGINE_CONFIG_PATH),
+                )
+                .arg(
+                    Arg::with_name("diagnostics-image-name")
+                        .long("diagnostics-image-name")
+                        .value_name("IMAGE_NAME")
+                        .help("Sets the name of the azureiotedge-diagnostics image.")
+                        .takes_value(true)
+                        .default_value(&default_diagnostics_image_name),
+                )
+                .arg(
+                    Arg::with_name("expected-iotedged-version")
+                        .long("expected-iotedged-version")
+                        .value_name("VERSION")
+                        .help("Sets the expected version of the iotedged binary. Defaults to the value contained in <http://aka.ms/latest-iotedge-stable>")
+                        .takes_value(true),
+                )
+                .arg(
+                    Arg::with_name("iotedged")
+                        .long("iotedged")
+                        .value_name("PATH_TO_IOTEDGED")
+                        .help("Sets the path of the iotedged binary.")
+                        .takes_value(true)
+                        .default_value(
+                            if cfg!(windows) { r"C:\Program Files\iotedge\iotedged.exe" } else { "/usr/bin/iotedged" }
+                        ),
+                )
+                .arg(
+                    Arg::with_name("ntp-server")
+                        .long("ntp-server")
+                        .value_name("NTP_SERVER")
+                        .help("Sets the NTP server to use when checking host local time.")
+                        .takes_value(true)
+                        .default_value("pool.ntp.org:123"),
+                )
+                .arg(
+                    Arg::with_name("verbose")
+                        .long("verbose")
+                        .value_name("VERBOSE")
+                        .help("Increases verbosity of output.")
+                        .takes_value(false),
+                ),
+        )
         .subcommand(SubCommand::with_name("list").about("List modules"))
         .subcommand(
             SubCommand::with_name("restart")
@@ -115,6 +193,32 @@ fn run() -> Result<(), Error> {
     let mut tokio_runtime = tokio::runtime::Runtime::new().context(ErrorKind::InitializeTokio)?;
 
     match matches.subcommand() {
+        ("check", Some(args)) => tokio_runtime.block_on(
+            Check::new(
+                args.value_of_os("config-file")
+                    .expect("arg has a default value")
+                    .to_os_string()
+                    .into(),
+                args.value_of_os("container-engine-config-file")
+                    .expect("arg has a default value")
+                    .to_os_string()
+                    .into(),
+                args.value_of("diagnostics-image-name")
+                    .expect("arg has a default value")
+                    .to_string(),
+                args.value_of("expected-iotedged-version")
+                    .map(ToOwned::to_owned),
+                args.value_of_os("iotedged")
+                    .expect("arg has a default value")
+                    .to_os_string()
+                    .into(),
+                args.value_of("ntp-server")
+                    .expect("arg has a default value")
+                    .to_string(),
+                args.occurrences_of("verbose") > 0,
+            )
+            .and_then(|mut check| check.execute()),
+        ),
         ("list", Some(_args)) => tokio_runtime.block_on(List::new(runtime, io::stdout()).execute()),
         ("restart", Some(args)) => tokio_runtime.block_on(
             Restart::new(
diff --git a/edgelet/iotedged/Cargo.toml b/edgelet/iotedged/Cargo.toml
index d26965820e5..c5eba19c114 100644
--- a/edgelet/iotedged/Cargo.toml
+++ b/edgelet/iotedged/Cargo.toml
@@ -7,24 +7,21 @@ publish = false
 [dependencies]
 base64 = "0.9"
 clap = "2.31"
-config = "0.8"
 env_logger = "0.5"
 failure = "0.1"
 futures = "0.1"
 hyper = "0.12.17"
 log = "0.4"
-serde = "1.0"
-serde_derive = "1.0"
 serde_json = "1.0"
 sha2 = "0.7.0"
 tokio = "0.1.8"
 tokio-signal = "0.2"
 url = "1.7"
-url_serde = "0.2"
 
 hsm = { path = "../hsm-rs"}
 dps = { path = "../dps" }
 docker = { path = "../docker-rs" }
+edgelet-config = { path = "../edgelet-config" }
 edgelet-core = { path = "../edgelet-core" }
 edgelet-docker = { path = "../edgelet-docker" }
 edgelet-hsm = { path = "../edgelet-hsm" }
diff --git a/edgelet/iotedged/src/app.rs b/edgelet/iotedged/src/app.rs
index 5846b636a2a..e9dccf0b5cf 100644
--- a/edgelet/iotedged/src/app.rs
+++ b/edgelet/iotedged/src/app.rs
@@ -1,16 +1,20 @@
 // Copyright (c) Microsoft. All rights reserved.
 
+use std::path::Path;
+
 use clap::{App, Arg, ArgMatches};
+use failure::ResultExt;
+
+use edgelet_config::Settings;
 use edgelet_core;
 use edgelet_docker::DockerConfig;
 
-use error::Error;
+use error::{Error, ErrorKind, InitializeErrorReason};
 use logging;
-use settings::Settings;
 
 pub fn create_base_app<'a, 'b>() -> App<'a, 'b> {
     App::new(crate_name!())
-        .version(crate_version!())
+        .version(edgelet_core::version())
         .author(crate_authors!("\n"))
         .about(crate_description!())
         .arg(
@@ -50,9 +54,10 @@ pub fn init_common<'a>() -> Result<(Settings<DockerConfig>, ArgMatches<'a>), Err
     let matches = create_app().get_matches();
     let settings = {
         let config_file = matches
-            .value_of("config-file")
+            .value_of_os("config-file")
             .and_then(|name| {
-                info!("Using config file: {}", name);
+                let name = Path::new(name);
+                info!("Using config file: {}", name.display());
                 Some(name)
             })
             .or_else(|| {
@@ -60,7 +65,8 @@ pub fn init_common<'a>() -> Result<(Settings<DockerConfig>, ArgMatches<'a>), Err
                 None
             });
 
-        Settings::<DockerConfig>::new(config_file)?
+        Settings::<DockerConfig>::new(config_file)
+            .context(ErrorKind::Initialize(InitializeErrorReason::LoadSettings))?
     };
 
     Ok((settings, matches))
@@ -84,13 +90,15 @@ pub fn init() -> Result<Settings<DockerConfig>, Error> {
 #[cfg(not(target_os = "windows"))]
 pub fn init() -> Result<Settings<DockerConfig>, Error> {
     logging::init();
+    let (settings, _) = init_common()?;
     log_banner();
-    init_common().map(|(settings, _)| settings)
+    Ok(settings)
 }
 
 #[cfg(target_os = "windows")]
 pub fn init_win_svc() -> Result<Settings<DockerConfig>, Error> {
     logging::init_win_log();
+    let (settings, _) = init_common()?;
     log_banner();
-    init_common().map(|(settings, _)| settings)
+    Ok(settings)
 }
diff --git a/edgelet/iotedged/src/lib.rs b/edgelet/iotedged/src/lib.rs
index 93826909402..c0bc8063861 100644
--- a/edgelet/iotedged/src/lib.rs
+++ b/edgelet/iotedged/src/lib.rs
@@ -12,9 +12,9 @@
 extern crate base64;
 #[macro_use]
 extern crate clap;
-extern crate config;
 extern crate docker;
 extern crate dps;
+extern crate edgelet_config;
 extern crate edgelet_core;
 extern crate edgelet_docker;
 extern crate edgelet_hsm;
@@ -34,17 +34,13 @@ extern crate iothubservice;
 #[macro_use]
 extern crate log;
 extern crate provisioning;
-extern crate serde;
-extern crate sha2;
-#[macro_use]
-extern crate serde_derive;
 extern crate serde_json;
+extern crate sha2;
 #[cfg(test)]
 extern crate tempdir;
 extern crate tokio;
 extern crate tokio_signal;
 extern crate url;
-extern crate url_serde;
 #[cfg(target_os = "windows")]
 #[macro_use]
 extern crate windows_service;
@@ -54,7 +50,6 @@ extern crate win_logger;
 pub mod app;
 mod error;
 pub mod logging;
-pub mod settings;
 pub mod signal;
 pub mod workload;
 
@@ -82,20 +77,22 @@ use url::Url;
 
 use docker::models::HostConfig;
 use dps::DPS_API_VERSION;
+use edgelet_config::{Dps, Manual, Provisioning, Settings, DEFAULT_CONNECTION_STRING};
 use edgelet_core::crypto::{
     Activate, CreateCertificate, Decrypt, DerivedKeyStore, Encrypt, GetTrustBundle, KeyIdentity,
     KeyStore, MasterEncryptionKey, MemoryKey, MemoryKeyStore, Sign, IOTEDGED_CA_ALIAS,
 };
 use edgelet_core::watchdog::Watchdog;
-use edgelet_core::WorkloadConfig;
-use edgelet_core::{CertificateIssuer, CertificateProperties, CertificateType};
-use edgelet_core::{ModuleRuntime, ModuleSpec};
+use edgelet_core::{
+    CertificateIssuer, CertificateProperties, CertificateType, ModuleRuntime, ModuleSpec, UrlExt,
+    WorkloadConfig, UNIX_SCHEME,
+};
 use edgelet_docker::{DockerConfig, DockerModuleRuntime};
 use edgelet_hsm::tpm::{TpmKey, TpmKeyStore};
 use edgelet_hsm::Crypto;
 use edgelet_http::client::{Client as HttpClient, ClientImpl};
 use edgelet_http::logging::LoggingService;
-use edgelet_http::{HyperExt, MaybeProxyClient, UrlExt, API_VERSION};
+use edgelet_http::{HyperExt, MaybeProxyClient, API_VERSION};
 use edgelet_http_mgmt::ManagementService;
 use edgelet_http_workload::WorkloadService;
 use edgelet_iothub::{HubIdentityManager, SasTokenSource};
@@ -107,7 +104,6 @@ use provisioning::provisioning::{
     Provision, ProvisioningResult,
 };
 
-use settings::{Dps, Manual, Provisioning, Settings, DEFAULT_CONNECTION_STRING};
 use workload::WorkloadData;
 
 pub use self::error::{Error, ErrorKind, InitializeErrorReason};
@@ -171,7 +167,6 @@ const EDGE_NETWORKID_KEY: &str = "NetworkId";
 const API_VERSION_KEY: &str = "IOTEDGE_APIVERSION";
 
 const IOTHUB_API_VERSION: &str = "2017-11-08-preview";
-const UNIX_SCHEME: &str = "unix";
 
 /// This is the name of the provisioning backup file
 const EDGE_PROVISIONING_BACKUP_FILENAME: &str = "provisioning_backup.json";
@@ -429,7 +424,7 @@ where
     info!("Detecting if configuration file has changed...");
     let path = subdir_path.join(filename);
     let mut reconfig_reqd = false;
-    let diff = settings.diff_with_cached(path)?;
+    let diff = settings.diff_with_cached(&path);
     if diff {
         info!("Change to configuration file detected.");
         reconfig_reqd = true;
@@ -613,9 +608,13 @@ fn manual_provision(
     provisioning: &Manual,
     tokio_runtime: &mut tokio::runtime::Runtime,
 ) -> Result<(DerivedKeyStore<MemoryKey>, ProvisioningResult, MemoryKey), Error> {
-    let manual = ManualProvisioning::new(provisioning.device_connection_string()).context(
-        ErrorKind::Initialize(InitializeErrorReason::ManualProvisioningClient),
-    )?;
+    let (key, device_id, hub) =
+        provisioning
+            .parse_device_connection_string()
+            .context(ErrorKind::Initialize(
+                InitializeErrorReason::ManualProvisioningClient,
+            ))?;
+    let manual = ManualProvisioning::new(key, device_id, hub);
     let memory_hsm = MemoryKeyStore::new();
     let provision = manual
         .provision(memory_hsm.clone())
@@ -991,6 +990,7 @@ where
 mod tests {
     use std::fmt;
     use std::io::Read;
+    use std::path::Path;
 
     use tempdir::TempDir;
 
@@ -1002,14 +1002,14 @@ mod tests {
     use super::*;
 
     #[cfg(unix)]
-    static SETTINGS: &str = "test/linux/sample_settings.yaml";
+    static SETTINGS: &str = "../edgelet-config/test/linux/sample_settings.yaml";
     #[cfg(unix)]
-    static SETTINGS1: &str = "test/linux/sample_settings1.yaml";
+    static SETTINGS1: &str = "../edgelet-config/test/linux/sample_settings1.yaml";
 
     #[cfg(windows)]
-    static SETTINGS: &str = "test/windows/sample_settings.yaml";
+    static SETTINGS: &str = "../edgelet-config/test/windows/sample_settings.yaml";
     #[cfg(windows)]
-    static SETTINGS1: &str = "test/windows/sample_settings1.yaml";
+    static SETTINGS1: &str = "../edgelet-config/test/windows/sample_settings1.yaml";
 
     #[derive(Clone, Copy, Debug, Fail)]
     pub struct Error;
@@ -1020,12 +1020,6 @@ mod tests {
         }
     }
 
-    // impl From<Error> for super::Error {
-    //     fn from(_error: Error) -> Self {
-    //         super::Error::from(ErrorKind::Var)
-    //     }
-    // }
-
     struct TestCrypto {}
 
     impl MasterEncryptionKey for TestCrypto {
@@ -1071,7 +1065,7 @@ mod tests {
     #[test]
     fn settings_first_time_creates_backup() {
         let tmp_dir = TempDir::new("blah").unwrap();
-        let settings = Settings::<DockerConfig>::new(Some(SETTINGS)).unwrap();
+        let settings = Settings::<DockerConfig>::new(Some(Path::new(SETTINGS))).unwrap();
         let config = TestConfig::new("microsoft/test-image".to_string());
         let state = ModuleRuntimeState::default();
         let module: TestModule<Error> =
@@ -1103,7 +1097,7 @@ mod tests {
     #[test]
     fn settings_change_creates_new_backup() {
         let tmp_dir = TempDir::new("blah").unwrap();
-        let settings = Settings::<DockerConfig>::new(Some(SETTINGS)).unwrap();
+        let settings = Settings::<DockerConfig>::new(Some(Path::new(SETTINGS))).unwrap();
         let config = TestConfig::new("microsoft/test-image".to_string());
         let state = ModuleRuntimeState::default();
         let module: TestModule<Error> =
@@ -1126,7 +1120,7 @@ mod tests {
             .read_to_string(&mut written)
             .unwrap();
 
-        let settings1 = Settings::<DockerConfig>::new(Some(SETTINGS1)).unwrap();
+        let settings1 = Settings::<DockerConfig>::new(Some(Path::new(SETTINGS1))).unwrap();
         let mut tokio_runtime = tokio::runtime::Runtime::new().unwrap();
         check_settings_state(
             tmp_dir.path().to_path_buf(),
diff --git a/edgelet/mini-sntp/Cargo.toml b/edgelet/mini-sntp/Cargo.toml
new file mode 100644
index 00000000000..d29c2e07723
--- /dev/null
+++ b/edgelet/mini-sntp/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "mini-sntp"
+version = "0.1.0"
+authors = ["Azure IoT Edge Devs"]
+edition = "2018"
+publish = false
+
+[dependencies]
+chrono = "0.4"
+failure = "0.1"
diff --git a/edgelet/mini-sntp/src/error.rs b/edgelet/mini-sntp/src/error.rs
new file mode 100644
index 00000000000..063c453023b
--- /dev/null
+++ b/edgelet/mini-sntp/src/error.rs
@@ -0,0 +1,126 @@
+#[derive(Debug)]
+pub struct Error(failure::Context<ErrorKind>);
+
+impl Error {
+    pub fn kind(&self) -> &ErrorKind {
+        self.0.get_context()
+    }
+}
+
+impl std::fmt::Display for Error {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl failure::Fail for Error {
+    fn cause(&self) -> Option<&dyn failure::Fail> {
+        self.0.cause()
+    }
+
+    fn backtrace(&self) -> Option<&failure::Backtrace> {
+        self.0.backtrace()
+    }
+}
+
+impl From<ErrorKind> for Error {
+    fn from(kind: ErrorKind) -> Self {
+        Error(failure::Context::new(kind))
+    }
+}
+
+impl From<failure::Context<ErrorKind>> for Error {
+    fn from(context: failure::Context<ErrorKind>) -> Self {
+        Error(context)
+    }
+}
+
+#[derive(Debug)]
+pub enum ErrorKind {
+    BadServerResponse(BadServerResponseReason),
+    BindLocalSocket,
+    ReceiveServerResponse(std::io::Error),
+    ResolveNtpPoolHostname(Option<std::io::Error>),
+    SendClientRequest(std::io::Error),
+    SetReadTimeoutOnSocket,
+    SetWriteTimeoutOnSocket,
+}
+
+impl std::fmt::Display for ErrorKind {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            ErrorKind::BadServerResponse(reason) => {
+                write!(f, "could not parse NTP server response: {}", reason)
+            }
+            ErrorKind::BindLocalSocket => write!(f, "could not bind local UDP socket"),
+            ErrorKind::ReceiveServerResponse(err) => {
+                write!(f, "could not receive NTP server response: {}", err)
+            }
+            ErrorKind::ResolveNtpPoolHostname(Some(err)) => {
+                write!(f, "could not resolve NTP pool hostname: {}", err)
+            }
+            ErrorKind::ResolveNtpPoolHostname(None) => {
+                write!(f, "could not resolve NTP pool hostname: no addresses found")
+            }
+            ErrorKind::SendClientRequest(err) => {
+                write!(f, "could not send SNTP client request: {}", err)
+            }
+            ErrorKind::SetReadTimeoutOnSocket => {
+                write!(f, "could not set read timeout on local UDP socket")
+            }
+            ErrorKind::SetWriteTimeoutOnSocket => {
+                write!(f, "could not set write timeout on local UDP socket")
+            }
+        }
+    }
+}
+
+impl failure::Fail for ErrorKind {
+    fn cause(&self) -> Option<&dyn failure::Fail> {
+        #[allow(clippy::match_same_arms)]
+        match self {
+            ErrorKind::BadServerResponse(_) => None,
+            ErrorKind::BindLocalSocket => None,
+            ErrorKind::ReceiveServerResponse(err) => Some(err),
+            ErrorKind::ResolveNtpPoolHostname(Some(err)) => Some(err),
+            ErrorKind::ResolveNtpPoolHostname(None) => None,
+            ErrorKind::SendClientRequest(err) => Some(err),
+            ErrorKind::SetReadTimeoutOnSocket => None,
+            ErrorKind::SetWriteTimeoutOnSocket => None,
+        }
+    }
+}
+
+#[derive(Clone, Copy, Debug)]
+pub enum BadServerResponseReason {
+    LeapIndicator(u8),
+    OriginateTimestamp {
+        expected: chrono::DateTime<chrono::Utc>,
+        actual: chrono::DateTime<chrono::Utc>,
+    },
+    Mode(u8),
+    VersionNumber(u8),
+}
+
+impl std::fmt::Display for BadServerResponseReason {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            BadServerResponseReason::LeapIndicator(leap_indicator) => {
+                write!(f, "invalid value of leap indicator {}", leap_indicator)
+            }
+            BadServerResponseReason::OriginateTimestamp { expected, actual } => write!(
+                f,
+                "expected originate timestamp to be {} but it was {}",
+                expected, actual
+            ),
+            BadServerResponseReason::Mode(mode) => {
+                write!(f, "expected mode to be 4 but it was {}", mode)
+            }
+            BadServerResponseReason::VersionNumber(version_number) => write!(
+                f,
+                "expected version number to be 3 but it was {}",
+                version_number
+            ),
+        }
+    }
+}
diff --git a/edgelet/mini-sntp/src/lib.rs b/edgelet/mini-sntp/src/lib.rs
new file mode 100644
index 00000000000..168f054c35b
--- /dev/null
+++ b/edgelet/mini-sntp/src/lib.rs
@@ -0,0 +1,340 @@
+#![deny(rust_2018_idioms, warnings)]
+#![deny(clippy::all, clippy::pedantic)]
+#![allow(
+    clippy::doc_markdown,
+    clippy::module_name_repetitions,
+    clippy::use_self
+)]
+
+use std::net::{SocketAddr, ToSocketAddrs, UdpSocket};
+
+use failure::ResultExt;
+
+mod error;
+pub use error::{BadServerResponseReason, Error, ErrorKind};
+
+/// The result of [`query`]
+#[derive(Debug)]
+pub struct SntpTimeQueryResult {
+    pub local_clock_offset: chrono::Duration,
+    pub round_trip_delay: chrono::Duration,
+}
+
+/// Executes an SNTP query against the NTPv3 server at the given address.
+///
+/// Ref: <https://tools.ietf.org/html/rfc2030>
+pub fn query<A>(addr: &A) -> Result<SntpTimeQueryResult, Error>
+where
+    A: ToSocketAddrs,
+{
+    let addr = addr
+        .to_socket_addrs()
+        .map_err(|err| ErrorKind::ResolveNtpPoolHostname(Some(err)))?
+        .next()
+        .ok_or(ErrorKind::ResolveNtpPoolHostname(None))?;
+
+    let socket = UdpSocket::bind("0.0.0.0:0").context(ErrorKind::BindLocalSocket)?;
+    socket
+        .set_read_timeout(Some(std::time::Duration::from_secs(10)))
+        .context(ErrorKind::SetReadTimeoutOnSocket)?;
+    socket
+        .set_write_timeout(Some(std::time::Duration::from_secs(10)))
+        .context(ErrorKind::SetWriteTimeoutOnSocket)?;
+
+    let mut num_retries_remaining = 3;
+    while num_retries_remaining > 0 {
+        match query_inner(&socket, addr) {
+            Ok(result) => return Ok(result),
+            Err(err) => {
+                let is_retriable = match err.kind() {
+                    ErrorKind::SendClientRequest(err) | ErrorKind::ReceiveServerResponse(err) => {
+                        err.kind() == std::io::ErrorKind::TimedOut || // Windows
+                        err.kind() == std::io::ErrorKind::WouldBlock // Unix
+                    }
+
+                    _ => false,
+                };
+                if is_retriable {
+                    num_retries_remaining -= 1;
+                    if num_retries_remaining == 0 {
+                        return Err(err);
+                    }
+                } else {
+                    return Err(err);
+                }
+            }
+        }
+    }
+
+    unreachable!();
+}
+
+fn query_inner(socket: &UdpSocket, addr: SocketAddr) -> Result<SntpTimeQueryResult, Error> {
+    let request_transmit_timestamp = {
+        let (buf, request_transmit_timestamp) = create_client_request();
+
+        #[cfg(test)]
+        std::thread::sleep(std::time::Duration::from_secs(5)); // simulate network delay
+
+        let mut buf = &buf[..];
+        while !buf.is_empty() {
+            let sent = socket
+                .send_to(buf, addr)
+                .map_err(ErrorKind::SendClientRequest)?;
+            buf = &buf[sent..];
+        }
+
+        request_transmit_timestamp
+    };
+
+    let result = {
+        let mut buf = [0_u8; 48];
+
+        {
+            let mut buf = &mut buf[..];
+            while !buf.is_empty() {
+                let (received, received_from) = socket
+                    .recv_from(buf)
+                    .map_err(ErrorKind::ReceiveServerResponse)?;
+                if received_from == addr {
+                    buf = &mut buf[received..];
+                }
+            }
+        }
+
+        #[cfg(test)]
+        std::thread::sleep(std::time::Duration::from_secs(5)); // simulate network delay
+
+        parse_server_response(buf, request_transmit_timestamp)?
+    };
+
+    Ok(result)
+}
+
+fn create_client_request() -> ([u8; 48], chrono::DateTime<chrono::Utc>) {
+    let sntp_epoch = sntp_epoch();
+
+    let mut buf = [0_u8; 48];
+    buf[0] = 0b00_011_011; // version_number: 3, mode: 3 (client)
+
+    let transmit_timestamp = chrono::Utc::now();
+
+    #[cfg(test)]
+    let transmit_timestamp = transmit_timestamp - chrono::Duration::seconds(30); // simulate unsynced local clock
+
+    let mut duration_since_sntp_epoch = transmit_timestamp - sntp_epoch;
+
+    let integral_part = duration_since_sntp_epoch.num_seconds();
+    duration_since_sntp_epoch =
+        duration_since_sntp_epoch - chrono::Duration::seconds(integral_part);
+
+    assert!(integral_part >= 0 && integral_part < i64::from(u32::max_value()));
+    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
+    let integral_part = (integral_part as u32).to_be_bytes();
+    buf[40..44].copy_from_slice(&integral_part[..]);
+
+    let fractional_part = duration_since_sntp_epoch
+        .num_nanoseconds()
+        .expect("can't overflow nanoseconds");
+    let fractional_part = (fractional_part << 32) / 1_000_000_000;
+    assert!(fractional_part >= 0 && fractional_part < i64::from(u32::max_value()));
+    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
+    let fractional_part = (fractional_part as u32).to_be_bytes();
+    buf[44..48].copy_from_slice(&fractional_part[..]);
+
+    let packet = Packet::parse(buf, sntp_epoch);
+    #[cfg(test)]
+    let packet = dbg!(packet);
+
+    // Re-extract transmit timestamp from the packet. This may not be the same as the original `transmit_timestamp`
+    // that was serialized into the packet due to rounding. Specifically, it's usually off by 1ns.
+    let transmit_timestamp = packet.transmit_timestamp;
+
+    (buf, transmit_timestamp)
+}
+
+fn parse_server_response(
+    buf: [u8; 48],
+    request_transmit_timestamp: chrono::DateTime<chrono::Utc>,
+) -> Result<SntpTimeQueryResult, Error> {
+    let sntp_epoch = sntp_epoch();
+
+    let destination_timestamp = chrono::Utc::now();
+
+    #[cfg(test)]
+    let destination_timestamp = destination_timestamp - chrono::Duration::seconds(30); // simulate unsynced local clock
+
+    let packet = Packet::parse(buf, sntp_epoch);
+    #[cfg(test)]
+    let packet = dbg!(packet);
+
+    match packet.leap_indicator {
+        0..=2 => (),
+        leap_indicator => {
+            return Err(
+                ErrorKind::BadServerResponse(BadServerResponseReason::LeapIndicator(
+                    leap_indicator,
+                ))
+                .into(),
+            );
+        }
+    };
+
+    if packet.version_number != 3 {
+        return Err(
+            ErrorKind::BadServerResponse(BadServerResponseReason::VersionNumber(
+                packet.version_number,
+            ))
+            .into(),
+        );
+    }
+
+    if packet.mode != 4 {
+        return Err(ErrorKind::BadServerResponse(BadServerResponseReason::Mode(packet.mode)).into());
+    }
+
+    if packet.mode != 4 {
+        return Err(ErrorKind::BadServerResponse(BadServerResponseReason::Mode(packet.mode)).into());
+    }
+
+    if packet.originate_timestamp != request_transmit_timestamp {
+        return Err(
+            ErrorKind::BadServerResponse(BadServerResponseReason::OriginateTimestamp {
+                expected: request_transmit_timestamp,
+                actual: packet.originate_timestamp,
+            })
+            .into(),
+        );
+    }
+
+    Ok(SntpTimeQueryResult {
+        local_clock_offset: ((packet.receive_timestamp - request_transmit_timestamp)
+            + (packet.transmit_timestamp - destination_timestamp))
+            / 2,
+
+        round_trip_delay: (destination_timestamp - request_transmit_timestamp)
+            - (packet.receive_timestamp - packet.transmit_timestamp),
+    })
+}
+
+fn sntp_epoch() -> chrono::DateTime<chrono::Utc> {
+    chrono::DateTime::<chrono::Utc>::from_utc(
+        chrono::NaiveDate::from_ymd(1900, 1, 1).and_time(chrono::NaiveTime::from_hms(0, 0, 0)),
+        chrono::Utc,
+    )
+}
+
+#[derive(Debug)]
+struct Packet {
+    leap_indicator: u8,
+    version_number: u8,
+    mode: u8,
+    stratum: u8,
+    poll_interval: u8,
+    precision: u8,
+    root_delay: u32,
+    root_dispersion: u32,
+    reference_identifier: u32,
+    reference_timestamp: chrono::DateTime<chrono::Utc>,
+    originate_timestamp: chrono::DateTime<chrono::Utc>,
+    receive_timestamp: chrono::DateTime<chrono::Utc>,
+    transmit_timestamp: chrono::DateTime<chrono::Utc>,
+}
+
+impl Packet {
+    fn parse(buf: [u8; 48], sntp_epoch: chrono::DateTime<chrono::Utc>) -> Self {
+        let leap_indicator = (buf[0] & 0b11_000_000) >> 6;
+        let version_number = (buf[0] & 0b00_111_000) >> 3;
+        let mode = buf[0] & 0b00_000_111;
+        let stratum = buf[1];
+        let poll_interval = buf[2];
+        let precision = buf[3];
+        let root_delay = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
+        let root_dispersion = u32::from_be_bytes([buf[8], buf[9], buf[10], buf[11]]);
+        let reference_identifier = u32::from_be_bytes([buf[12], buf[13], buf[14], buf[15]]);
+        let reference_timestamp = deserialize_timestamp(
+            [
+                buf[16], buf[17], buf[18], buf[19], buf[20], buf[21], buf[22], buf[23],
+            ],
+            sntp_epoch,
+        );
+        let originate_timestamp = deserialize_timestamp(
+            [
+                buf[24], buf[25], buf[26], buf[27], buf[28], buf[29], buf[30], buf[31],
+            ],
+            sntp_epoch,
+        );
+        let receive_timestamp = deserialize_timestamp(
+            [
+                buf[32], buf[33], buf[34], buf[35], buf[36], buf[37], buf[38], buf[39],
+            ],
+            sntp_epoch,
+        );
+        let transmit_timestamp = deserialize_timestamp(
+            [
+                buf[40], buf[41], buf[42], buf[43], buf[44], buf[45], buf[46], buf[47],
+            ],
+            sntp_epoch,
+        );
+
+        Packet {
+            leap_indicator,
+            version_number,
+            mode,
+            stratum,
+            poll_interval,
+            precision,
+            root_delay,
+            root_dispersion,
+            reference_identifier,
+            reference_timestamp,
+            originate_timestamp,
+            receive_timestamp,
+            transmit_timestamp,
+        }
+    }
+}
+
+fn deserialize_timestamp(
+    raw: [u8; 8],
+    sntp_epoch: chrono::DateTime<chrono::Utc>,
+) -> chrono::DateTime<chrono::Utc> {
+    let integral_part = i64::from(u32::from_be_bytes([raw[0], raw[1], raw[2], raw[3]]));
+    let fractional_part = i64::from(u32::from_be_bytes([raw[4], raw[5], raw[6], raw[7]]));
+    let duration_since_sntp_epoch = chrono::Duration::nanoseconds(
+        integral_part * 1_000_000_000 + ((fractional_part * 1_000_000_000) >> 32),
+    );
+
+    sntp_epoch + duration_since_sntp_epoch
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn it_works() -> Result<(), Error> {
+        let SntpTimeQueryResult {
+            local_clock_offset,
+            round_trip_delay,
+        } = query(&("pool.ntp.org", 123))?;
+
+        println!("local clock offset: {}", local_clock_offset);
+        println!("round-trip delay: {}", round_trip_delay);
+
+        assert!(
+            (local_clock_offset - chrono::Duration::seconds(30))
+                .num_seconds()
+                .abs()
+                < 1
+        );
+        assert!(
+            (round_trip_delay - chrono::Duration::seconds(10))
+                .num_seconds()
+                .abs()
+                < 1
+        );
+
+        Ok(())
+    }
+}
diff --git a/edgelet/provisioning/Cargo.toml b/edgelet/provisioning/Cargo.toml
index 4ee31481fa9..00b07923fa0 100644
--- a/edgelet/provisioning/Cargo.toml
+++ b/edgelet/provisioning/Cargo.toml
@@ -4,12 +4,10 @@ version = "0.1.0"
 authors = ["Azure IoT Edge Devs"]
 
 [dependencies]
-base64 = "0.9"
 bytes = "0.4"
 failure = "0.1"
 futures = "0.1"
 log = "0.4"
-regex = "0.2"
 serde = "1.0"
 serde_derive = "1.0"
 serde_json = "1.0"
@@ -25,3 +23,5 @@ edgelet-utils = { path = "../edgelet-utils" }
 [dev_dependencies]
 tempdir = "0.3.7"
 tokio = "0.1.8"
+
+edgelet-config = { path = "../edgelet-config" }
diff --git a/edgelet/provisioning/src/error.rs b/edgelet/provisioning/src/error.rs
index 475cfd29385..96c08037cab 100644
--- a/edgelet/provisioning/src/error.rs
+++ b/edgelet/provisioning/src/error.rs
@@ -12,15 +12,6 @@ pub struct Error {
 
 #[derive(Clone, Copy, Debug, Fail)]
 pub enum ErrorKind {
-    #[fail(display = "The Connection String is missing required parameter {}", _0)]
-    ConnStringMissingRequiredParameter(&'static str),
-
-    #[fail(
-        display = "The Connection String has a malformed value for parameter {}.",
-        _0
-    )]
-    ConnStringMalformedParameter(&'static str),
-
     #[fail(display = "Could not backup provisioning result")]
     CouldNotBackup,
 
@@ -30,11 +21,6 @@ pub enum ErrorKind {
     #[fail(display = "Could not initialize DPS provisioning client")]
     DpsInitialization,
 
-    #[fail(
-        display = "The Connection String is empty or invalid. Please update the config.yaml and provide the IoTHub connection information."
-    )]
-    InvalidConnString,
-
     #[fail(display = "Could not provision device")]
     Provision,
 }
diff --git a/edgelet/provisioning/src/lib.rs b/edgelet/provisioning/src/lib.rs
index cb498820dea..5c0cd5acad3 100644
--- a/edgelet/provisioning/src/lib.rs
+++ b/edgelet/provisioning/src/lib.rs
@@ -4,14 +4,12 @@
 #![deny(clippy::all, clippy::pedantic)]
 #![allow(clippy::module_name_repetitions, clippy::use_self)]
 
-extern crate base64;
 extern crate bytes;
 extern crate failure;
 extern crate futures;
 extern crate hsm;
 #[macro_use]
 extern crate log;
-extern crate regex;
 #[macro_use]
 extern crate serde_derive;
 extern crate serde_json;
@@ -22,6 +20,8 @@ extern crate tokio;
 extern crate url;
 
 extern crate dps;
+#[cfg(test)]
+extern crate edgelet_config;
 extern crate edgelet_core;
 extern crate edgelet_hsm;
 extern crate edgelet_http;
diff --git a/edgelet/provisioning/src/provisioning.rs b/edgelet/provisioning/src/provisioning.rs
index 7421e0f32b1..6ce358e7262 100644
--- a/edgelet/provisioning/src/provisioning.rs
+++ b/edgelet/provisioning/src/provisioning.rs
@@ -1,17 +1,14 @@
 // Copyright (c) Microsoft. All rights reserved.
 
-use std::collections::HashMap;
 use std::fs::File;
 use std::io::{Read, Write};
 use std::path::PathBuf;
 
-use base64;
 use bytes::Bytes;
 
 use failure::{Fail, ResultExt};
 use futures::future::Either;
 use futures::{future, Future, IntoFuture};
-use regex::Regex;
 use serde_json;
 use url::Url;
 
@@ -19,19 +16,11 @@ use dps::registration::{DpsAuthKind, DpsClient, DpsTokenSource};
 use edgelet_core::crypto::{Activate, KeyIdentity, KeyStore, MemoryKey, MemoryKeyStore};
 use edgelet_hsm::tpm::{TpmKey, TpmKeyStore};
 use edgelet_http::client::{Client as HttpClient, ClientImpl};
-use edgelet_utils::{ensure_not_empty_with_context, log_failure};
+use edgelet_utils::log_failure;
 use error::{Error, ErrorKind};
 use hsm::TpmKey as HsmTpmKey;
 use log::Level;
 
-const DEVICEID_KEY: &str = "DeviceId";
-const HOSTNAME_KEY: &str = "HostName";
-const SHAREDACCESSKEY_KEY: &str = "SharedAccessKey";
-
-const DEVICEID_REGEX: &str = r"^[A-Za-z0-9\-:.+%_#*?!(),=@;$']{1,128}$";
-const HOSTNAME_REGEX: &str = r"^[a-zA-Z0-9_\-\.]+$";
-const SHAREDACCESSKEY_REGEX: &str = r"^.+$";
-
 #[derive(Clone, Serialize, Deserialize)]
 pub struct ProvisioningResult {
     device_id: String,
@@ -71,68 +60,12 @@ pub struct ManualProvisioning {
 }
 
 impl ManualProvisioning {
-    pub fn new(conn_string: &str) -> Result<Self, Error> {
-        ensure_not_empty_with_context(&conn_string, || ErrorKind::InvalidConnString)?;
-
-        let hash_map = ManualProvisioning::parse_conn_string(&conn_string)?;
-
-        let key = hash_map.get(SHAREDACCESSKEY_KEY).ok_or(
-            ErrorKind::ConnStringMissingRequiredParameter(SHAREDACCESSKEY_KEY),
-        )?;
-        let key_regex = Regex::new(SHAREDACCESSKEY_REGEX)
-            .expect("This hard-coded regex is expected to be valid.");
-        if !key_regex.is_match(&key) {
-            return Err(Error::from(ErrorKind::ConnStringMalformedParameter(
-                SHAREDACCESSKEY_REGEX,
-            )));
-        }
-        let key = MemoryKey::new(base64::decode(&key).context(
-            ErrorKind::ConnStringMalformedParameter(SHAREDACCESSKEY_REGEX),
-        )?);
-
-        let device_id = hash_map
-            .get(DEVICEID_KEY)
-            .ok_or(ErrorKind::ConnStringMissingRequiredParameter(DEVICEID_KEY))?;
-        let device_id_regex =
-            Regex::new(DEVICEID_REGEX).expect("This hard-coded regex is expected to be valid.");
-        if !device_id_regex.is_match(&device_id) {
-            return Err(Error::from(ErrorKind::ConnStringMalformedParameter(
-                DEVICEID_KEY,
-            )));
-        }
-
-        let hub = hash_map
-            .get(HOSTNAME_KEY)
-            .ok_or(ErrorKind::ConnStringMissingRequiredParameter(HOSTNAME_KEY))?;
-        let hub_regex =
-            Regex::new(HOSTNAME_REGEX).expect("This hard-coded regex is expected to be valid.");
-        if !hub_regex.is_match(&hub) {
-            return Err(Error::from(ErrorKind::ConnStringMalformedParameter(
-                HOSTNAME_KEY,
-            )));
-        }
-
-        let result = ManualProvisioning {
+    pub fn new(key: MemoryKey, device_id: String, hub: String) -> Self {
+        ManualProvisioning {
             key,
-            device_id: device_id.to_owned(),
-            hub: hub.to_owned(),
-        };
-        Ok(result)
-    }
-
-    fn parse_conn_string(conn_string: &str) -> Result<HashMap<String, String>, Error> {
-        let mut hash_map = HashMap::new();
-        let parts: Vec<&str> = conn_string.split(';').collect();
-        for p in parts {
-            let s: Vec<&str> = p.split('=').collect();
-            match s[0] {
-                SHAREDACCESSKEY_KEY | DEVICEID_KEY | HOSTNAME_KEY => {
-                    hash_map.insert(s[0].to_string(), s[1].to_string());
-                }
-                _ => (), // Ignore extraneous component in the connection string
-            }
+            device_id,
+            hub,
         }
-        Ok(hash_map)
     }
 }
 
@@ -407,6 +340,8 @@ mod tests {
     use tempdir::TempDir;
     use tokio;
 
+    use edgelet_config::{Manual, ParseManualDeviceConnectionStringError};
+
     use error::ErrorKind;
 
     struct TestProvisioning {}
@@ -439,24 +374,29 @@ mod tests {
         }
     }
 
+    fn parse_connection_string(
+        s: &str,
+    ) -> Result<ManualProvisioning, ParseManualDeviceConnectionStringError> {
+        let (key, device_id, hub) = Manual::new(s.to_string()).parse_device_connection_string()?;
+        Ok(ManualProvisioning::new(key, device_id, hub))
+    }
+
     #[test]
     fn manual_get_credentials_success() {
         let provisioning =
-            ManualProvisioning::new("HostName=test.com;DeviceId=test;SharedAccessKey=test");
-        assert_eq!(provisioning.is_ok(), true);
+            parse_connection_string("HostName=test.com;DeviceId=test;SharedAccessKey=test")
+                .unwrap();
         let memory_hsm = MemoryKeyStore::new();
-        let task =
-            provisioning
-                .unwrap()
-                .provision(memory_hsm.clone())
-                .then(|result| match result {
-                    Ok(result) => {
-                        assert_eq!(result.hub_name, "test.com".to_string());
-                        assert_eq!(result.device_id, "test".to_string());
-                        Ok::<_, Error>(())
-                    }
-                    Err(err) => panic!("Unexpected {:?}", err),
-                });
+        let task = provisioning
+            .provision(memory_hsm.clone())
+            .then(|result| match result {
+                Ok(result) => {
+                    assert_eq!(result.hub_name, "test.com".to_string());
+                    assert_eq!(result.device_id, "test".to_string());
+                    Ok::<_, Error>(())
+                }
+                Err(err) => panic!("Unexpected {:?}", err),
+            });
         tokio::runtime::current_thread::Runtime::new()
             .unwrap()
             .block_on(task)
@@ -465,42 +405,36 @@ mod tests {
 
     #[test]
     fn manual_malformed_conn_string_gets_error() {
-        let test = ManualProvisioning::new("HostName=test.com;DeviceId=test;");
-        assert_eq!(test.is_err(), true);
+        let test = parse_connection_string("HostName=test.com;DeviceId=test;");
+        assert!(test.is_err());
     }
 
     #[test]
     fn connection_string_split_success() {
         let provisioning =
-            ManualProvisioning::new("HostName=test.com;DeviceId=test;SharedAccessKey=test");
-        assert_eq!(provisioning.is_ok(), true);
+            parse_connection_string("HostName=test.com;DeviceId=test;SharedAccessKey=test")
+                .unwrap();
         let memory_hsm = MemoryKeyStore::new();
-        let task = provisioning
-            .unwrap()
-            .provision(memory_hsm.clone())
-            .then(|result| {
-                let result = result.expect("Unexpected");
-                assert_eq!(result.hub_name, "test.com".to_string());
-                assert_eq!(result.device_id, "test".to_string());
-                Ok::<_, Error>(())
-            });
+        let task = provisioning.provision(memory_hsm.clone()).then(|result| {
+            let result = result.expect("Unexpected");
+            assert_eq!(result.hub_name, "test.com".to_string());
+            assert_eq!(result.device_id, "test".to_string());
+            Ok::<_, Error>(())
+        });
         tokio::runtime::current_thread::Runtime::new()
             .unwrap()
             .block_on(task)
             .unwrap();
 
         let provisioning1 =
-            ManualProvisioning::new("DeviceId=test;SharedAccessKey=test;HostName=test.com");
-        assert_eq!(provisioning1.is_ok(), true);
-        let task1 = provisioning1
-            .unwrap()
-            .provision(memory_hsm.clone())
-            .then(|result| {
-                let result = result.expect("Unexpected");
-                assert_eq!(result.hub_name, "test.com".to_string());
-                assert_eq!(result.device_id, "test".to_string());
-                Ok::<_, Error>(())
-            });
+            parse_connection_string("DeviceId=test;SharedAccessKey=test;HostName=test.com")
+                .unwrap();
+        let task1 = provisioning1.provision(memory_hsm.clone()).then(|result| {
+            let result = result.expect("Unexpected");
+            assert_eq!(result.hub_name, "test.com".to_string());
+            assert_eq!(result.device_id, "test".to_string());
+            Ok::<_, Error>(())
+        });
         tokio::runtime::current_thread::Runtime::new()
             .unwrap()
             .block_on(task1)
@@ -509,14 +443,15 @@ mod tests {
 
     #[test]
     fn connection_string_split_error() {
-        let test1 = ManualProvisioning::new("DeviceId=test;SharedAccessKey=test");
-        assert_eq!(test1.is_err(), true);
-        let test2 = ManualProvisioning::new(
+        let test1 = parse_connection_string("DeviceId=test;SharedAccessKey=test");
+        assert!(test1.is_err());
+
+        let test2 = parse_connection_string(
             "HostName=test.com;Extra=something;DeviceId=test;SharedAccessKey=test",
-        );
-        assert_eq!(test2.is_ok(), true);
+        )
+        .unwrap();
         let memory_hsm = MemoryKeyStore::new();
-        let task1 = test2.unwrap().provision(memory_hsm.clone()).then(|result| {
+        let task1 = test2.provision(memory_hsm.clone()).then(|result| {
             let result = result.expect("Unexpected");
             assert_eq!(result.hub_name, "test.com".to_string());
             assert_eq!(result.device_id, "test".to_string());
diff --git a/scripts/linux/buildDiagnostics.sh b/scripts/linux/buildDiagnostics.sh
new file mode 100755
index 00000000000..d25aa25e9e3
--- /dev/null
+++ b/scripts/linux/buildDiagnostics.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+# This script builds the iotedge-diagnostics binary that goes into the azureiotedge-diagnostics image,
+# for each supported arch (x86_64, arm32v7).
+# It then publishes the binaries along with their corresponding dockerfiles to the publish directory,
+# so that buildImage.sh can build the container image.
+
+set -euo pipefail
+
+VERSIONINFO_FILE_PATH=$BUILD_REPOSITORY_LOCALPATH/versionInfo.json
+PUBLISH_FOLDER=$BUILD_BINARIESDIRECTORY/publish
+export VERSION="$(cat "$VERSIONINFO_FILE_PATH" | jq '.version' -r)"
+
+mkdir -p $PUBLISH_FOLDER/azureiotedge-diagnostics/
+cp -R $BUILD_REPOSITORY_LOCALPATH/edgelet/iotedge-diagnostics/docker $PUBLISH_FOLDER/azureiotedge-diagnostics/docker
+
+cd "$BUILD_REPOSITORY_LOCALPATH/edgelet"
+
+cross build -p iotedge-diagnostics --release --target x86_64-unknown-linux-musl
+strip $BUILD_REPOSITORY_LOCALPATH/edgelet/target/x86_64-unknown-linux-musl/release/iotedge-diagnostics
+cp $BUILD_REPOSITORY_LOCALPATH/edgelet/target/x86_64-unknown-linux-musl/release/iotedge-diagnostics $PUBLISH_FOLDER/azureiotedge-diagnostics/docker/linux/amd64/
+
+cross build -p iotedge-diagnostics --release --target armv7-unknown-linux-musleabihf
+arm-linux-gnueabihf-strip $BUILD_REPOSITORY_LOCALPATH/edgelet/target/armv7-unknown-linux-musleabihf/release/iotedge-diagnostics
+cp $BUILD_REPOSITORY_LOCALPATH/edgelet/target/armv7-unknown-linux-musleabihf/release/iotedge-diagnostics $PUBLISH_FOLDER/azureiotedge-diagnostics/docker/linux/arm32v7/