Skip to content

Commit

Permalink
Merge pull request #60 from danielemery/19-add-an-endpoint-for-sharin…
Browse files Browse the repository at this point in the history
…g-known_hosts

Add an endpoint for sharing known hosts
  • Loading branch information
danielemery authored Sep 16, 2024
2 parents c7555e8 + 53897fa commit 062d38b
Show file tree
Hide file tree
Showing 15 changed files with 386 additions and 8 deletions.
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ curl "https://keys.demery.net/keys?user=demery&allOf=oak&noneOf=disabled" > ~/.s
cat ~/.ssh/authorized_keys
```

### Update known hosts file

_Replaces the `known_hosts` file with the hosts in your keys instance_

```sh
# Consider backup first
cp ~/.ssh/known_hosts ~/.ssh/known_hosts.`date '+%Y-%m-%d__%H_%M_%S'`.backup
# Override file with the hosts from the keys instance
curl http://localhost:8000/known_hosts > ~/.ssh/known_hosts
```

## Running / Installation

### Configuration File
Expand All @@ -49,7 +60,7 @@ Regardless of the method of deployment, the `keys` application requires a config
yaml file containing the list of keys to be served. An example file can be found
in `./examples/keys-config.yaml`.

The config file contains two main sections:
The config file contains three main sections:

- `ssh-keys`: A list of public ssh keys with the following fields:
- name: The name of the key (this will be used as the `@host` in the
Expand All @@ -62,6 +73,20 @@ The config file contains two main sections:
- name: The name of the key (this will be used in the route and as the
filename if you download the key)
- key: The public key itself
- `known-hosts`: A list of known hosts with the following fields:
- name: Optional name for the entry, it's not used in the `known_hosts` file
and is just for your records
- hosts: A list of hostnames or IPs that the key(s) should be associated with
- keys: A list of known keys that should be associated with the host, with the
following fields:
- type: The type of key (eg `ssh-rsa`)
- key: The public key itself
- comment: Optional comment for the entry (will be appended to the key in
the `known_hosts` file)
- revoked: Optional boolean to indicate that the key should be considered
revoked (adds the @revoked marker in the known hosts file)
- cert-authority: Optional boolean to indicate that the key is a certificate
authority (adds the @cert-authority marker in the known hosts file)

### Helm

Expand Down
25 changes: 25 additions & 0 deletions examples/keys-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,28 @@ pgp-keys:
wACJnWrUiXq9+uZlBCcYKxWiVKa4ahMSENU9mulAs6uCO5NSCs1s
=NiXa
-----END PGP PUBLIC KEY BLOCK-----
known-hosts:
- name: GitHub
hosts:
- github.com
keys:
- type: ssh-ed25519
key: "AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl"
- type: ecdsa-sha2-nistp256
key: "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg="
- type: ssh-rsa
key: AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=
- name: Oak
hosts:
- oak.home.demery.net
- oak
- 10.0.6.0
keys:
- type: ssh-ed25519
key: AAAAC3NzaC1lZDI1NTE5AAAAIPmEbpWjOUk56EsQTHz/6KWH6eeS7Xpd/Qa0G/uxylGQ
- type: ssh-rsa
key: AAAAB3NzaC1yc2EAAAADAQABAAACAQDExzmtD9//MOK2JHT7o04iRmgTgdRJEmtmhb6C53czRDEz3x41Zf/qTjHb97yCtiHDwvL+sX/PAxSDrvmCjMIj2Mbl3i5D45s4YG3Cbg55QLr+lt8rwp5eZSlFamfKpygEju6nwOAUariaRRhrLxWK5tk8YF0v03vQMeKaDFz6iM9vWnbV1mnOwG5r2CPdO6JsbPwRrNlhj1mcOESgr+jOuksDCG36t/ttti4MbHpVJbpiIvg8I92dgn+fPKrlPdSkzIsPG719doIjc3bw8ea3KMucLAMJRqZBYyXfX8Y0HNGQPjSKx+pxiuwHL4ZHxbi+BhQGPvMmEo7DYYqy6PmX1E7O+B3OP2TIPUCJsNu8wIE38HNrbADlMC9ovo0IFc+nQUtpggeT4WvEne4lYqLibR4Rwb4yxv4HiDhcpoBakdCIzvj0cQFi5UVzQ1joT3qWXyHADjHRCYAo0lJ1UPAcFvljwJR1yN5mPSvcplQ1w/WPzkFQn5fTFf2i/KM2EbHNH5aWbI7sIW8zlL6pGjlI3gsiBXVFfCQ6Xt3Di9ruR0cYfTmkgLgblcgJDgq1vLq1Ipe56/u48Ehe0WiEpIzJ3bFUSVHYX7zvoSVTBP3DuOGifvk1s6Q4MfnuOdr7u6OETSssemzHI43d0NINs4ketX/Q2/aS8/5ugpiJUfgSMQ==
- type: ssh-ed25519
key: AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
revoked: true
comment: 'old leaked key'
7 changes: 7 additions & 0 deletions fixtures/invalid-known-hosts.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ssh-keys:
- name: key-one
key: "ssh-rsa my-key-one"
user: joeblogs
known-hosts:
- name: example
hosts: hello, error
14 changes: 14 additions & 0 deletions fixtures/valid-known-hosts.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
ssh-keys:
- name: key-one
key: "ssh-rsa my-key-one"
user: joeblogs
known-hosts:
- name: example
hosts:
- example.com
keys:
- type: ssh-ed25519
key: "fake-ed25519-key"
comment: An example key
- type: ssh-rsa
key: "fake rsa key"
10 changes: 7 additions & 3 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "./src/routes/pgp/serve_pgp.ts";
import { serveKeys } from "./src/routes/keys/serve-keys.ts";
import { serveHome } from "./src/routes/serve-home.ts";
import { serveKnownHosts } from "./src/routes/known_hosts/serve-known-hosts.ts";

const environment = parseEnvironmentVariables(Deno.env.toObject());

Expand All @@ -25,9 +26,10 @@ if (environment.SENTRY_DSN) {
});
}

const { "ssh-keys": sshKeys, "pgp-keys": pgpKeys } = await loadConfig(
environment.CONFIG_PATH,
);
const { "ssh-keys": sshKeys, "pgp-keys": pgpKeys, "known-hosts": knownHosts } =
await loadConfig(
environment.CONFIG_PATH,
);

start(
environment.PORT,
Expand All @@ -39,8 +41,10 @@ start(
getPGPTarget,
servePGPKey,
servePGPKeyList,
serveKnownHosts,
sshKeys,
pgpKeys,
knownHosts,
instanceName: environment.INSTANCE_NAME,
},
environment.KEYS_VERSION,
Expand Down
2 changes: 2 additions & 0 deletions src/common/test_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export const emptyDependencies: ServerDependencies = {
getPGPTarget: () => undefined,
servePGPKey: () => new Response(""),
servePGPKeyList: () => new Response(""),
serveKnownHosts: () => new Response(""),
sshKeys: [],
pgpKeys: [],
knownHosts: [],
instanceName: "unit-tests",
};
77 changes: 77 additions & 0 deletions src/config/load_config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ Deno.test("loadConfig: must load valid config with ssh keys", async () => {
},
],
"pgp-keys": [],
"known-hosts": [],
});
});

Expand All @@ -121,5 +122,81 @@ fake2
`,
},
],
"known-hosts": [],
});
});

Deno.test("loadConfig: must throw zod error if known hosts are not valid", async () => {
await assertRejects(
async () => {
await loadConfig("./fixtures/invalid-known-hosts.yaml");
},
ZodError,
`{
"code": "invalid_type",
"expected": "array",
"received": "string",
"path": [
"known-hosts",
0,
"hosts"
],
"message": "Expected array, received string"
}`,
);
await assertRejects(
async () => {
await loadConfig("./fixtures/invalid-known-hosts.yaml");
},
ZodError,
`{
"code": "invalid_type",
"expected": "array",
"received": "undefined",
"path": [
"known-hosts",
0,
"keys"
],
"message": "Required"
}`,
);
});

Deno.test("loadConfig: must load valid config with known hosts", async () => {
const config = await loadConfig("./fixtures/valid-known-hosts.yaml");
assertEquals(config, {
"ssh-keys": [
{
key: "ssh-rsa my-key-one",
name: "key-one",
tags: [],
user: "joeblogs",
},
],
"pgp-keys": [],
"known-hosts": [
{
name: "example",
hosts: [
"example.com",
],
keys: [
{
comment: "An example key",
key: "fake-ed25519-key",
revoked: false,
"cert-authority": false,
type: "ssh-ed25519",
},
{
key: "fake rsa key",
revoked: false,
"cert-authority": false,
type: "ssh-rsa",
},
],
},
],
});
});
12 changes: 12 additions & 0 deletions src/config/load_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,20 @@ const configSchema = z.object({
),
"pgp-keys": z.array(z.object({ name: z.string(), key: z.string() }))
.optional().default([]),
"known-hosts": z.array(z.object({
name: z.string().optional(),
hosts: z.array(z.string()),
keys: z.array(z.object({
type: z.string(),
key: z.string(),
comment: z.string().optional(),
revoked: z.boolean().optional().default(false),
"cert-authority": z.boolean().optional().default(false),
})),
})).optional().default([]),
});

export type Config = z.infer<typeof configSchema>;
export type PublicSSHKey = z.infer<typeof configSchema>["ssh-keys"][number];
export type PGPKey = z.infer<typeof configSchema>["pgp-keys"][number];
export type KnownHost = z.infer<typeof configSchema>["known-hosts"][number];
101 changes: 101 additions & 0 deletions src/routes/known_hosts/serve-known-hosts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { assertEquals } from "https://deno.land/[email protected]/assert/mod.ts";
import { serveKnownHosts } from "./serve-known-hosts.ts";
import { emptyDependencies } from "../../common/test_helpers.ts";

Deno.test("serveKnownHosts (plain): must serve empty string if there are no dependencies", async () => {
const actual = serveKnownHosts("unit-tests", {
...emptyDependencies,
}, "text/plain");
assertEquals(actual.status, 200);
assertEquals(actual.statusText, "OK");
assertEquals(await actual.text(), "");
});

Deno.test("serveKnownHosts (plain): must serve known hosts", async () => {
const knownHosts = [
{
name: "host1",
hosts: ["host1.com", "host2.com"],
keys: [
{
type: "ssh-rsa",
key: "key1",
comment: "comment1",
revoked: false,
"cert-authority": false,
},
{
type: "ssh-rsa",
key: "key2",
revoked: false,
"cert-authority": false,
},
],
},
{
name: "host2",
hosts: ["host3.com"],
keys: [
{
type: "ssh-rsa",
key: "key3",
revoked: false,
"cert-authority": false,
},
],
},
];
const actual = serveKnownHosts("unit-tests", {
...emptyDependencies,
knownHosts,
}, "text/plain");
assertEquals(actual.status, 200);
assertEquals(actual.statusText, "OK");
assertEquals(
await actual.text(),
`host1.com,host2.com ssh-rsa key1 comment1
host1.com,host2.com ssh-rsa key2
host3.com ssh-rsa key3`,
);
});

Deno.test("serveKnownHosts (plain): must serve known hosts with markers", async () => {
const knownHosts = [
{
name: "host1",
hosts: ["host1.com"],
keys: [
{
type: "ssh-rsa",
key: "key1",
revoked: false,
"cert-authority": true,
},
{
type: "ssh-rsa",
key: "key2",
revoked: true,
"cert-authority": false,
comment: "revoked 2024-09-11",
},
],
},
];
const actual = serveKnownHosts("unit-tests", {
...emptyDependencies,
knownHosts,
}, "text/plain");
assertEquals(actual.status, 200);
assertEquals(actual.statusText, "OK");
assertEquals(
await actual.text(),
`@cert-authority host1.com ssh-rsa key1
@revoked host1.com ssh-rsa key2 revoked 2024-09-11`,
);
});

Deno.test("serveKnownHosts (plain): must return NotAcceptable for unsupported content type", () => {
const actual = serveKnownHosts("unit-tests", emptyDependencies, "text/html");
assertEquals(actual.status, 406);
assertEquals(actual.statusText, "Not Acceptable");
});
Loading

0 comments on commit 062d38b

Please sign in to comment.