diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2015 Justin Leitgeb
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/spec/Spec.hs b/spec/Spec.hs
new file mode 100644
--- /dev/null
+++ b/spec/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
diff --git a/spec/fixtures/sshd_config b/spec/fixtures/sshd_config
new file mode 100644
--- /dev/null
+++ b/spec/fixtures/sshd_config
@@ -0,0 +1,88 @@
+# Package generated configuration file
+# See the sshd_config(5) manpage for details
+
+# What ports, IPs and protocols we listen for
+Port 22
+# Use these options to restrict which interfaces/protocols sshd will bind to
+#ListenAddress ::
+#ListenAddress 0.0.0.0
+Protocol 2
+# HostKeys for protocol version 2
+HostKey /etc/ssh/ssh_host_rsa_key
+HostKey /etc/ssh/ssh_host_dsa_key
+HostKey /etc/ssh/ssh_host_ecdsa_key
+#Privilege Separation is turned on for security
+UsePrivilegeSeparation yes
+
+# Lifetime and size of ephemeral version 1 server key
+KeyRegenerationInterval 3600
+ServerKeyBits 768
+
+# Logging
+SyslogFacility AUTH
+LogLevel INFO
+
+# Authentication:
+LoginGraceTime 120
+PermitRootLogin yes
+PermitRootLogin no
+StrictModes yes
+
+RSAAuthentication yes
+PubkeyAuthentication yes
+#AuthorizedKeysFile	%h/.ssh/authorized_keys
+
+# Don't read the user's ~/.rhosts and ~/.shosts files
+IgnoreRhosts yes
+# For this to work you will also need host keys in /etc/ssh_known_hosts
+RhostsRSAAuthentication no
+# similar for protocol version 2
+HostbasedAuthentication no
+# Uncomment if you don't trust ~/.ssh/known_hosts for RhostsRSAAuthentication
+#IgnoreUserKnownHosts yes
+
+# To enable empty passwords, change to yes (NOT RECOMMENDED)
+PermitEmptyPasswords no
+
+# Change to yes to enable challenge-response passwords (beware issues with
+# some PAM modules and threads)
+ChallengeResponseAuthentication no
+
+# Change to no to disable tunnelled clear text passwords
+PasswordAuthentication no
+
+# Kerberos options
+#KerberosAuthentication no
+#KerberosGetAFSToken no
+#KerberosOrLocalPasswd yes
+#KerberosTicketCleanup yes
+
+# GSSAPI options
+#GSSAPIAuthentication no
+#GSSAPICleanupCredentials yes
+
+X11Forwarding yes
+X11DisplayOffset 10
+PrintMotd no
+PrintLastLog yes
+TCPKeepAlive yes
+#UseLogin no
+
+#MaxStartups 10:30:60
+#Banner /etc/issue.net
+
+# Allow client to pass locale environment variables
+AcceptEnv LANG LC_*
+
+Subsystem sftp /usr/lib/openssh/sftp-server
+
+# Set this to 'yes' to enable PAM authentication, account processing,
+# and session processing. If this is enabled, PAM authentication will
+# be allowed through the ChallengeResponseAuthentication and
+# PasswordAuthentication.  Depending on your PAM configuration,
+# PAM authentication via ChallengeResponseAuthentication may bypass
+# the setting of "PermitRootLogin without-password".
+# If you just want the PAM account and session checks to run without
+# PAM authentication, then enable this but set PasswordAuthentication
+# and ChallengeResponseAuthentication to 'no'.
+UsePAM yes
diff --git a/src/Main.hs b/src/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/Main.hs
@@ -0,0 +1,135 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main where
+
+import System.Console.GetOpt
+
+import System.Environment (getArgs, getProgName)
+import System.Exit (ExitCode(..), exitWith, exitSuccess)
+
+import Paths_sshd_lint (version)
+
+import Data.Version (showVersion)
+
+import Data.KeywordArgs.Parse
+import System.SshdLint.Check
+
+import qualified Data.Map as Map
+import qualified Data.Set as Set
+
+import Control.Monad (unless, when)
+import System.Nagios.Plugin
+
+import Text.Parsec (ParseError, parse)
+
+data Options = Options
+    { optShowVersion :: Bool
+    , optShowHelp    :: Bool
+    , optNagios      :: Bool
+    , optFile        :: FilePath
+    } deriving Show
+
+defaultOptions :: Options
+defaultOptions = Options
+    { optShowVersion = False
+    , optShowHelp    = False
+    , optNagios      = False
+    , optFile        = "/etc/ssh/sshd_config"
+    }
+
+options :: [OptDescr (Options -> Options)]
+options =
+  [ Option "V" ["version"] (NoArg (\opts -> opts { optShowVersion = True }))
+    "show version number"
+
+  , Option "f" ["file"]
+    (ReqArg (\f opts -> opts { optFile = f })
+     "FILE") "input FILE"
+
+  , Option "h" ["help"]    (NoArg (\opts -> opts { optShowHelp = True }))
+    "show help"
+
+  , Option "n" ["nagios"]    (NoArg (\opts -> opts { optNagios = True }))
+    "display output in Nagios-consumable format"
+  ]
+
+showHelp :: IO ()
+showHelp = do
+  prg <- getProgName
+  putStrLn (usageInfo prg options)
+  exitSuccess
+
+headerMessage :: String -> String
+headerMessage progName = "Usage: " ++ progName ++ " [OPTION...]"
+
+lintOpts :: [String] -> IO (Options, [String])
+lintOpts argv = do
+  name <- getProgName
+
+  case getOpt Permute options argv of
+    (o, n, []  ) -> return (foldl (flip id) defaultOptions o, n)
+    (_, _, errs) ->
+      ioError (userError (concat errs ++ usageInfo (headerMessage name) options))
+
+parseConfig :: String -> Either ParseError [(String, [String])]
+parseConfig = parse configParser "(unknown)"
+
+printResult :: String -> [String] -> IO ()
+printResult msg errs =
+  unless (null errs) $
+    do
+      name <- getProgName
+      putStrLn $ name ++ " encountered the following " ++ msg ++ ":\n"
+      mapM_ (\err -> putStrLn $ "\t* " ++ err) errs
+      putStr "\n"
+
+runCheck :: Options -> IO ()
+runCheck opts = do
+
+  f <- readFile $ optFile opts
+
+  unless (optNagios opts) $ putStrLn ("Checking " ++ optFile opts ++ ":\n")
+
+  case parseConfig f of
+    Left e -> putStrLn $ "Parse error: " ++ show e
+    Right config -> do
+      let dupVals = Set.toList $ duplicatedValues $ map fst config
+          recs = recommendations (Map.fromList defaultAcceptedValues) $
+                 activeSettings config
+
+      unless (optNagios opts) $ do
+        printResult "insecure settings" recs
+        printResult "duplicate values" dupVals
+
+      if (not . null) dupVals || (not . null) recs
+        then if optNagios opts
+             then
+               runNagiosPlugin (sshdSafeCheck $ length $ recs ++ dupVals)
+
+             else do
+               putStrLn $ show (length (recs ++ dupVals)) ++ " suggestions"
+               exitWith (ExitFailure 1)
+
+        else
+          putStrLn "No suggestions"
+
+sshdSafeCheck :: Int -> NagiosPlugin ()
+sshdSafeCheck errorCount =
+  when (errorCount > 0) $
+    addResult Critical "Security warnings found in sshd_config"
+
+main :: IO ()
+main = do
+  opts <- getArgs >>= lintOpts
+
+  name <- getProgName
+
+  if optShowHelp $ fst opts
+  then
+    putStrLn $ usageInfo (headerMessage name) options
+  else
+    if optShowVersion $ fst opts then
+      putStrLn $ name ++ " " ++ showVersion version ++
+      " (C) 2015 Stack Builders Inc."
+    else
+      runCheck $ fst opts
diff --git a/src/System/SshdLint/Check.hs b/src/System/SshdLint/Check.hs
new file mode 100644
--- /dev/null
+++ b/src/System/SshdLint/Check.hs
@@ -0,0 +1,89 @@
+module System.SshdLint.Check
+       ( duplicatedValues
+       , activeSettings
+       , recommendations
+       , defaultAcceptedValues
+       , checkSafeSetting ) where
+
+import Data.Char (toLower)
+
+import qualified Data.Map as Map
+import qualified Data.Set as Set
+
+type RecommendedSettings = Map.Map String [String]
+type ActiveSettings      = Map.Map String [String]
+
+
+defaultAcceptedValues :: [(String, [String])]
+defaultAcceptedValues =
+  [ ("PermitEmptyPasswords", ["no"])
+  , ("PasswordAuthentication", ["no"])
+  , ("HostbasedAuthentication", ["no"])
+  , ("PermitRootLogin", ["no"])
+  , ("IgnoreRhosts", ["yes"])
+  , ("Protocol", ["2"])
+  , ("StrictModes", ["yes"])
+  , ("UsePrivilegeSeparation", ["yes"]) ]
+
+duplicatedValues :: [String] -> Set.Set String
+duplicatedValues values =
+  snd $ foldr checkDuplicate (Set.empty, Set.empty) values
+
+activeSettings :: [(String, [String])] -> ActiveSettings
+activeSettings =
+  foldr registerSetting Map.empty
+
+  where registerSetting (configOption, value) =
+          Map.insert (map toLower configOption) value
+
+-- | Given a collection of recommendation strings and a current
+-- setting, returns a new list of recommendations with new
+-- recommendations added depending on the setting of the given
+-- configOption and value.
+checkSafeSetting :: Map.Map String [String]
+                 -> String
+                 -> [String]
+                 -> [String]
+                 -> [String]
+checkSafeSetting recs configOption values previousRecommendations =
+  let recommendation = Map.lookup configOption recs in
+
+  case recommendation of
+    Nothing -> previousRecommendations
+    Just r -> if Set.fromList r == Set.fromList values then
+                previousRecommendations
+              else
+                previousRecommendations ++ [ configOption ++ " should be "
+                                             ++ show r ++ ", found " ++
+                                             show values ]
+
+
+recommendations :: RecommendedSettings -> ActiveSettings -> [String]
+recommendations recommendedSettings settings =
+  Map.foldWithKey (checkSafeSetting normalizedRecommendedSettings) []
+                  normalizedSettings
+
+  where normalizedRecommendedSettings = lowerCaseMapKeys recommendedSettings
+        normalizedSettings = lowerCaseMapKeys settings
+
+
+-- | Some values legitimately appear multiple times in the configuration.
+allowedDuplicates :: Set.Set String
+allowedDuplicates = Set.fromList ["hostkey"]
+
+checkDuplicate :: String
+               -> (Set.Set String, Set.Set String)
+               -> (Set.Set String, Set.Set String)
+
+checkDuplicate aValue (allValues, dupes) =
+  if Set.notMember loweredValue allowedDuplicates &&
+     Set.member loweredValue allValues then
+
+    (allValues, Set.insert loweredValue dupes)
+  else
+    (Set.insert loweredValue allValues, dupes)
+
+  where loweredValue = map toLower aValue
+
+lowerCaseMapKeys :: Map.Map String [String] -> Map.Map String [String]
+lowerCaseMapKeys = Map.mapKeys (map toLower)
diff --git a/sshd-lint.cabal b/sshd-lint.cabal
new file mode 100644
--- /dev/null
+++ b/sshd-lint.cabal
@@ -0,0 +1,52 @@
+name:                sshd-lint
+version:             0.1.0.1
+synopsis:            Check sshd configuration for adherence to best practices
+
+description: If not configured correctly, it may be easy for attackers
+             to gain access to a system. sshd-lint checks the sshd_config file
+             for adherence to best practices.
+
+license:             MIT
+license-file:        LICENSE
+author:              Justin Leitgeb
+maintainer:          justin@stackbuilders.com
+copyright:           2015 Stack Builders Inc.
+category:            System
+build-type:          Simple
+extra-source-files:  spec/fixtures/sshd_config
+cabal-version:       >=1.10
+
+executable sshd-lint
+  main-is:             Main.hs
+  other-modules:       System.SshdLint.Check
+
+  build-depends:         base >=4.5 && <4.8
+                       , parsec >= 3.1.0 && <= 3.2
+                       , keyword-args >= 0.2.0.1 && < 0.3.0.0
+                       , containers
+                       , nagios-check
+
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+  ghc-options:         -Wall
+
+
+
+test-suite sshd-lint-test
+  type: exitcode-stdio-1.0
+  hs-source-dirs: spec, src
+  main-is: Spec.hs
+  build-depends:       base >=4.5 && <4.8
+                       , parsec >= 3.1.0 && <= 3.2
+                       , keyword-args >= 0.2.0.0 && < 0.3.0.0
+                       , containers
+                       , nagios-check
+
+                       , hspec
+
+  default-language:    Haskell2010
+  ghc-options:         -Wall
+
+source-repository head
+  type:     git
+  location: https://github.com/stackbuilders/sshd-lint
