Skip to content

Commit

Permalink
Merge pull request #61 from vst/59-add-known-ssh-keys-to-hosts
Browse files Browse the repository at this point in the history
Add Host-Level Known SSH Public Keys
  • Loading branch information
vst authored Apr 9, 2024
2 parents 9f03e8c + 394efb6 commit e782b49
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 33 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,9 @@ plain host name. The configuration file looks like as follows:

```yaml
## config.yaml
## List of known SSH public keys to be added to the report.
##
## These can be then used by external programs of lhp Web UI to
## highlight if a host has an unknown authorized SSH public key.
## List of known SSH public keys for all hosts.
knownSshKeys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKq9bpy0IIfDnlgaTCQk0YhKyKFqInRjoqeIPlBuiFwS testing
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKq9bpy0IIfDnlgaTCQk0YhKyKFqInRjoqeIPlBuiFwS test-key-admin

## List of hosts to patrol
hosts:
Expand All @@ -120,6 +117,9 @@ hosts:
data:
owner: Client-1
cost: 50
## List of known SSH public keys for the host (optional)
knownSshKeys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGmlBxUagOqtWcW6B77TUL8li85ZNYx0tphd3TSx4SEB test-key-tenant
- name: otherhost
url: https://internal.documentation/hosts/otherhost
tags:
Expand Down
10 changes: 5 additions & 5 deletions config.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
## List of known SSH public keys to be added to the report.
##
## These can be then used by external programs of lhp Web UI to
## highlight if a host has an unknown authorized SSH public key.
## List of known SSH public keys for all hosts.
knownSshKeys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKq9bpy0IIfDnlgaTCQk0YhKyKFqInRjoqeIPlBuiFwS testing
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKq9bpy0IIfDnlgaTCQk0YhKyKFqInRjoqeIPlBuiFwS test-key-admin

## List of hosts to patrol
hosts:
Expand All @@ -27,6 +24,9 @@ hosts:
data:
owner: Client-1
cost: 50
## List of known SSH public keys for the host (optional)
knownSshKeys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGmlBxUagOqtWcW6B77TUL8li85ZNYx0tphd3TSx4SEB test-key-tenant
- name: otherhost
url: https://internal.documentation/hosts/otherhost
tags:
Expand Down
16 changes: 8 additions & 8 deletions src/Lhp/Cli.hs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import qualified Lhp.Config as Config
import qualified Lhp.Meta as Meta
import Lhp.Remote (compileReport)
import Lhp.Types (Report)
import qualified Lhp.Types as Types
import Options.Applicative ((<|>))
import qualified Options.Applicative as OA
import System.Exit (ExitCode (..))
Expand Down Expand Up @@ -82,13 +81,14 @@ doCompile cpath dests par = do
Right sr -> BLC.putStrLn (Aeson.encode sr) >> pure ExitSuccess
where
_mkHost d =
Types.Host
{ Types._hostName = d
, Types._hostSsh = Nothing
, Types._hostId = Nothing
, Types._hostUrl = Nothing
, Types._hostTags = []
, Types._hostData = Aeson.Null
Config.HostSpec
{ Config._hostSpecName = d
, Config._hostSpecSsh = Nothing
, Config._hostSpecId = Nothing
, Config._hostSpecUrl = Nothing
, Config._hostSpecTags = []
, Config._hostSpecData = Aeson.Null
, Config._hostSpecKnownSshKeys = []
}


Expand Down
36 changes: 33 additions & 3 deletions src/Lhp/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import qualified Data.Aeson as Aeson
import qualified Data.Text as T
import qualified Data.Yaml as Yaml
import GHC.Generics (Generic)
import qualified Lhp.Types as Types
import Zamazingo.Ssh (SshConfig)


-- | Data definition for application configuration.
data Config = Config
{ _configHosts :: ![Types.Host]
{ _configHosts :: ![HostSpec]
, _configKnownSshKeys :: ![T.Text]
}
deriving (Eq, Generic, Show)
Expand All @@ -31,7 +31,37 @@ instance ADC.HasCodec Config where
ADC.object "Config" $
Config
<$> ADC.optionalFieldWithDefault "hosts" [] "List of hosts." ADC..= _configHosts
<*> ADC.optionalFieldWithDefault "knownSshKeys" [] "List of hosts." ADC..= _configKnownSshKeys
<*> ADC.optionalFieldWithDefault "knownSshKeys" [] "Known SSH public keys for all hosts." ADC..= _configKnownSshKeys


-- | Data definition for host specification.
data HostSpec = HostSpec
{ _hostSpecName :: !T.Text
, _hostSpecSsh :: !(Maybe SshConfig)
, _hostSpecId :: !(Maybe T.Text)
, _hostSpecUrl :: !(Maybe T.Text)
, _hostSpecTags :: ![T.Text]
, _hostSpecData :: !Aeson.Value
, _hostSpecKnownSshKeys :: ![T.Text]
}
deriving (Eq, Generic, Show)
deriving (Aeson.FromJSON, Aeson.ToJSON) via (ADC.Autodocodec HostSpec)


instance ADC.HasCodec HostSpec where
codec =
_codec ADC.<?> "Host Specification"
where
_codec =
ADC.object "HostSpec" $
HostSpec
<$> ADC.requiredField "name" "Name of the host." ADC..= _hostSpecName
<*> ADC.optionalField "ssh" "SSH configuration." ADC..= _hostSpecSsh
<*> ADC.optionalField "id" "External identifier of the host." ADC..= _hostSpecId
<*> ADC.optionalField "url" "URL to external host information." ADC..= _hostSpecUrl
<*> ADC.optionalFieldWithDefault "tags" [] "Arbitrary tags for the host." ADC..= _hostSpecTags
<*> ADC.optionalFieldWithDefault "data" Aeson.Null "Arbitrary data for the host." ADC..= _hostSpecData
<*> ADC.optionalFieldWithDefault "knownSshKeys" [] "Known SSH public keys for the host." ADC..= _hostSpecKnownSshKeys


-- | Attempts to read a configuration file and return 'Config'.
Expand Down
27 changes: 23 additions & 4 deletions src/Lhp/Remote.hs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ compileReport par Config.Config {..} = do
pure Types.Report {..}
where
reporter = bool (fmap catMaybes . mapM go) (MP.mapM compileHostReport) par
go h@Types.Host {..} = do
liftIO (hPutStrLn stderr ("Patrolling " <> T.unpack _hostName))
go h@Config.HostSpec {..} = do
liftIO (hPutStrLn stderr ("Patrolling " <> T.unpack _hostSpecName))
res <- runExceptT (compileHostReport h)
case res of
Left err -> liftIO (BLC.hPutStrLn stderr (Aeson.encode err) >> pure Nothing)
Expand All @@ -61,9 +61,10 @@ compileReport par Config.Config {..} = do
compileHostReport
:: MonadIO m
=> MonadError LhpError m
=> Types.Host
=> Config.HostSpec
-> m Types.HostReport
compileHostReport h@Types.Host {..} = do
compileHostReport ch = do
h@Types.Host {..} <- _makeHostFromConfigHostSpec ch
kvs <- (++) <$> _fetchHostInfo h <*> _fetchHostCloudInfo h
let _hostReportHost = h
_hostReportHostname <- _toParseError _hostName $ _getParse pure "LHP_GENERAL_HOSTNAME" kvs
Expand All @@ -79,6 +80,24 @@ compileHostReport [email protected] {..} = do
pure Types.HostReport {..}


-- | Consumes a 'Config.HostSpec' and produces a 'Types.Host'.
_makeHostFromConfigHostSpec
:: MonadError LhpError m
=> MonadIO m
=> Config.HostSpec
-> m Types.Host
_makeHostFromConfigHostSpec Config.HostSpec {..} =
let _hostName = _hostSpecName
_hostSsh = _hostSpecSsh
_hostId = _hostSpecId
_hostUrl = _hostSpecUrl
_hostTags = _hostSpecTags
_hostData = _hostSpecData
in do
_hostKnownSshKeys <- mapM parseSshPublicKey _hostSpecKnownSshKeys
pure Types.Host {..}


-- * Errors


Expand Down
2 changes: 2 additions & 0 deletions src/Lhp/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ data Host = Host
, _hostUrl :: !(Maybe T.Text)
, _hostTags :: ![T.Text]
, _hostData :: !Aeson.Value
, _hostKnownSshKeys :: ![SshPublicKey]
}
deriving (Eq, Generic, Show)
deriving (Aeson.FromJSON, Aeson.ToJSON) via (ADC.Autodocodec Host)
Expand All @@ -68,6 +69,7 @@ instance ADC.HasCodec Host where
<*> ADC.optionalField "url" "URL to external host information." ADC..= _hostUrl
<*> ADC.optionalFieldWithDefault "tags" [] "Arbitrary tags for the host." ADC..= _hostTags
<*> ADC.optionalFieldWithDefault "data" Aeson.Null "Arbitrary data for the host." ADC..= _hostData
<*> ADC.optionalFieldWithDefault "knownSshKeys" [] "Known SSH public keys for the host." ADC..= _hostKnownSshKeys


-- * Host Report
Expand Down
2 changes: 1 addition & 1 deletion website/src/components/report/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export function TabShowHostDetails({
<div className="col-span-5">
{host.caseOf({
Nothing: () => <div className="p-4 text-red-400">Choose a host to view details.</div>,
Just: (x) => <ShowHostDetails host={x} />,
Just: (x) => <ShowHostDetails data={data} host={x} />,
})}
</div>
</div>
Expand Down
36 changes: 33 additions & 3 deletions website/src/components/report/ShowHostDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { LhpHostReport } from '@/lib/data';
import { LhpHostReport, LhpPatrolReport } from '@/lib/data';
import { Card, CardBody, CardHeader } from '@nextui-org/card';
import { Chip } from '@nextui-org/chip';
import { Listbox, ListboxItem } from '@nextui-org/listbox';
import Link from 'next/link';
import { toast } from 'react-toastify';
import { KVBox } from '../helpers';

export function ShowHostDetails({ host }: { host: LhpHostReport }) {
export function ShowHostDetails({ host, data }: { host: LhpHostReport; data: LhpPatrolReport }) {
const authorizedKeysPlanned = [...(data.knownSshKeys || []), ...(host.host.knownSshKeys || [])];
const authorizedKeysPlannedSet = new Set(authorizedKeysPlanned.map((x) => x.fingerprint));

return (
<div className="space-y-4 px-4 py-4">
<h1 className="flex flex-row items-center justify-between text-xl font-bold">
Expand Down Expand Up @@ -87,12 +90,39 @@ export function ShowHostDetails({ host }: { host: LhpHostReport }) {
</div>

<Card radius="sm" shadow="sm">
<CardHeader className="text-lg font-bold">Authorized SSH Keys</CardHeader>
<CardHeader className="text-lg font-bold">Authorized SSH Keys Found</CardHeader>

<CardBody>
<Listbox
items={host.authorizedSshKeys}
emptyContent={<span className="text-orange-400">No authorized SSH keys are found. Sounds weird?</span>}
>
{({ length, type, fingerprint, data, comment }) => (
<ListboxItem
key={data}
description={data}
startContent={authorizedKeysPlannedSet.has(fingerprint) ? <>🟢</> : <>🔴</>}
onPress={() => {
navigator.clipboard.writeText(data);
toast('SSH Key is copied to clipboard.');
}}
>
{`${type} (${length}) - ${fingerprint} - ${comment || ''}`}
</ListboxItem>
)}
</Listbox>
</CardBody>
</Card>

<Card radius="sm" shadow="sm">
<CardHeader className="text-lg font-bold">Authorized SSH Keys Planned</CardHeader>

<CardBody>
<Listbox
items={authorizedKeysPlanned}
emptyContent={
<span className="text-orange-400">No authorized SSH keys are found as planned. Sounds weird?</span>
}
>
{({ length, type, fingerprint, data, comment }) => (
<ListboxItem
Expand Down
29 changes: 25 additions & 4 deletions website/src/lib/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,27 @@ export const LHP_PATROL_REPORT_SCHEMA = {
properties: {
data: { $comment: 'Arbitrary data for the host.' },
id: { $comment: 'External identifier of the host.', type: 'string' },
knownSshKeys: {
$comment: 'Known SSH public keys for the host.',
items: {
$comment: 'SSH Public Key Information\nSshPublicKey',
properties: {
comment: { $comment: 'Comment on the public key.', type: 'string' },
data: { $comment: 'Original information.', type: 'string' },
fingerprint: { $comment: 'Fingerprint of the public key.', type: 'string' },
length: {
$comment: 'Length of the public key.',
maximum: 2147483647,
minimum: -2147483648,
type: 'number',
},
type: { $comment: 'Type of the public key.', type: 'string' },
},
required: ['fingerprint', 'comment', 'length', 'type', 'data'],
type: 'object',
},
type: 'array',
},
name: { $comment: 'Name of the host.', type: 'string' },
ssh: {
$comment: 'SSH configuration.\nSSH Configuration\nSshConfig',
Expand Down Expand Up @@ -283,10 +304,10 @@ export function buildSshKeysTable(data: LhpPatrolReport): SshKeysTable {
const keys: SshKeysTable = {};

// Lookup table for known SSH public key comments by their fingerprint:
const knownComments: Record<string, string> = data.knownSshKeys.reduce(
(acc, x) => ({ ...acc, [x.fingerprint]: x.comment || '<NO-COMMENT>' }),
{}
);
const knownComments: Record<string, string> = [
...data.knownSshKeys,
...data.hosts.reduce((acc, x) => [...acc, ...(x.host.knownSshKeys || [])], [] as typeof data.knownSshKeys),
].reduce((acc, x) => ({ ...acc, [x.fingerprint]: x.comment || '<NO-COMMENT>' }), {});

// Iterate over all SSH public keys for all hosts and populate our registry:
for (const host of data.hosts) {
Expand Down

0 comments on commit e782b49

Please sign in to comment.