summaryrefslogtreecommitdiff
path: root/sshca.md
blob: 7e91162b7985f39ed25213385dca0507b2f64c03 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
# Introduction

The `sshca` tool helps manage an SSH Certificate Authority (CA) and
create host and user certificates. Such certificates make using and
administering SSH less tedious and more secure, by removing the need
for users to check host keys, or maintain `authorized_keys` files.

An SSH CA is an SSH key dedicated to signing, or certifying, other SSH
keys. Such a signed key is called a certificate and is used together
with the private part of the certified key. The certificate is used
instead of the public key.

SSH clients and servers can be configured to trust certificates made
by one or more CA keys. This makes it possible for a client to trust a
server without asking the user to accept the host key for each new
server. A server can trust a client without having the client's public
key configured for that user in the `authorized_key` file. This
simplifies overall key management significantly, but requires creating
and managing CA keys and certificates.


## Host certificates

Traditionally, in the world of SSH, servers have host keys that rarely
change, but are generated separately for each host. When a user
accesses a host for the first time, at a given address, they are
presented with the host's public key, and need to manually,
laboriously, and usually insecurely, check that it's the right key for
that host.

This can be a risky situation: if an attacker manages to trick the
user's SSH client to show a key the attacker has generated, and the
user accepts it as the real one, the attacker can see – and
change – all the traffic going over the SSH connection. This
mostly nullifies the security benefit SSH is meant to provide. (Not
entirely: the connection is still protected against other attackers.)

In a situation where there are many hosts, or hosts gets recreated
often, or change address a lot, all of which happen when using
cloud technologies, the risky situation keeps happening frequently.
Not only is it risky, it is also tedious and cumbersome to the user.
If this keeps happening a lot, users are in effect trained to
automatically accept all host keys. This is an example of bad
usability being bad security.

The risky situation can be avoided by having the host keys be
communicated to all users ahead of time, but doing this in a secure
and convenient way is difficult. It is also unnecessary.

Using an SSH CA to certify SSH host keys means the user's SSH client
can trust it without asking the user to verify it. The client is
configured to trust any host certificate that can be verified using
the SSH CA public key. The CA public key still needs to be
communicated to the user in a secure way, but the CA key is only one
key and rarely changes, so the tiresome risky situation happens very
rarely. After the user has the CA key, an attacker can't trick the
user into accepting a false host key.

With host certificates, the SSH client never needs to ask its user if
the host key of a new host is valid, and the user never needs to try
to verify it. If the host's identity (host key or address) changes,
such as when a virtual machine is re-created, the client doesn't need
to bother the user about it, as long as the new identity gets a new
certificate.

Overall, this leads to a much smoother and more secure experience for
people using SSH.

## User certificates

Traditionally, a user authenticates themselves to an SSH server using
a password, or a user key. We will not discuss passwords here. You
should not use passwords. Your SSH server should not accept passwords.

A user SSH key is an SSH key pair of which the user has the private
part. The public part is added to the `~/.ssh/authorized_keys` file on
the server for the user's account. At login time, the client proves to
the server that it has the private key that corresponds to one of the
public keys in that file, and this proves to the server that the user
is authorized to log in. This is great, because the user does not need
to remember a strong password, nor type it in every time they log in,
and the server does not need to store the user's password at all, even
in an encrypted way.

The result is an easy, secure way for the user to log into the server.
However, this only works if the list of authorized keys is kept up to
date.

If the user needs to, for any reason, change to a new key, perhaps as
part of a regular key rotation strategy, the list of authorized keys
needs to be updated to add the new key, and remove the old key. This
needs to be done on each server the user uses. The update procedure is
often a risky, tedious step. If an attacker manages to get the
attacker's key into the list, they can log into the server as the
user. Given that the `authorized_keys` file is usually user-editable,
the user may add any SSH public keys to that file, including keys for
other people, or keys stored on machines that are insecure. The user
may do this intentionally, or because they've been tricked or coerced
into doing it.

An SSH CA can create a user certificate, which ties a user's SSH
public key to a username. An SSH server can be configured to trust
such certificates, made with specific CA keys, and to act as if that
user's public key is in their `authorized_keys` file, even if that
file doesn't exist. The result is that there is no need to maintain
that file. This also means it's feasible to revoke access with
specific certificates.

The user certificate replaces the public key in the SSH authentication
process. The user still needs the corresponding private key to
authenticate: the certificate itself is not enough.

Overall, this leads to system administrators having an easier way to
control who has access their servers over SSH.

## Certificate automation

Generating all these certificates can be done using the `ssh-keygen`
command line tool. However, it's just intricate enough that it becomes
tedious and cumbersome and thus error prone. The `sshca` tool makes it
easier.


## Configuring servers and user accounts

The `sshca` tool does not install host certificates on servers, nor
configure servers or user accounts to trust certificates made using
specific CA identities. The server system administrators and users
need to do that themselves.

## SSH CA vs SSHFP

Another approach is to distribute host keys via DNSSEC using SSHFP DNS
records. This requires DNSSEC to work for all clients, and only works
for verifying host identities, not user identities. However, they may
be easier to adopt for some organizations.


# The `sshca` command line tool

~~~dot
digraph "architecure" {

store [shape=folder label="Secure storage"]
pubkey [shape=note label="Public key"]
cakey [shape=note label="CA key"]
cert [shape=note label="Host or user certificate"]
sshca_keygen [shape=ellipse label="sshca \n create new CA key"]
sshca_import [shape=ellipse label="sshca \n import public key"]
sshca_export [shape=ellipse label="sshca \n produce certificate"]

pubkey -> sshca_import
sshca_keygen -> cakey
cakey -> store
sshca_import -> store
store -> sshca_export
sshca_export -> cert

}
~~~

The `sshca` tool maintains a secure storage of CA key pairs, and host
and user public keys, and can use the CA keys and the stored public
keys to generate host and user certificates.

## The store

**Security note:** The `sshca` tool maintains a *store* of SSH public
and private keys, as a directory on the local file system. This store
is assumed to be trusted: any key there is assumed to have been vetted
before being added. The user of the tool should ensure the store can
only be accessed by them and not by other parties. The security and
integrity of the SSH CA system maintained by `sshca` depends on that.

The store is kept in `~/.local/state/sshca` by default, but the
location can be configured via the tool configuration file.

The way the store is implemented should probably be improved.

## `sshca` built-in help

See the `sshca help` command for a list of its subcommands and how to
invoke everything. The `--help` option is an alias.

To avoid repetition and to avoid getting out of sync, this document
doesn't repeat all the commands and options.

## Revocations

The `sshca` tool does not support revoking certificates. Revocations
can be done manually using `ssh-keygen`, but it may be easier to use
certificates with short validity periods and creating and distributing
new certificates when needed. This is easier for host certificates,
which are under direct system administrator control.

For user certificates, a self-serve system would be good, but not
currently available. However, the system administrator can generate
and publish new user certificates frequently. User certificates are
not secret, and they're tightly tied to the user's private key. User
certificates are useless without the private key.

## Tool configuration

In `~/.config/sshca/config.yaml` (or other location as specified
according to the XDG directory standard), a configuration file can
specify:

* `store`

    Fully qualified, tilde-expanded path of the store location.


# Requirements for SSH CA automation

Tooling to automate SSH CA management needs to satisfy all the
following high-level requirements to be acceptable.

* **Secure.** SSH is meant to enable use of remote systems and file
  transfers in a secure manner. SSH CA is meant to improve security
  further. Any automation of SSH CA must not compromise on security.

* **Convenient to use** for both system administrators and end users. It
  is a fundamental realization that people will do things in the
  convenient manner, and security needs to enable that. SSH CA makes
  use of SSH more convenient, and automation for it must not squander
  that.

* **Convenient to integrate** with existing SSH management
  infrastructure.

The following sections document more detailed acceptance criteria
and how they are verified in an automated manner.

## Smoke test

This scenario verifies that the `sshca` command line tool can be
invoked at all, in the simplest possible ways.

~~~scenario
given an installed sshca
when I run sshca --help
then stdout contains "--help"
~~~

## Configuration lookup

This scenario verifies that the `sshca` command line tool can show its
actual configuration.

~~~scenario
given an installed sshca
when I run sshca config
then stdout contains "store"
~~~

## CA key management

It must be possible to manage multiple SSH CA keys. This scenario
verifies that `sshca` can create and remove CA keys.

Initially the store must be empty and have no CA keys.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml
when I run sshca ca list
then stdout is exactly ""
when I run sshca ca list --json
then stdout contains "[]"
~~~

~~~{#config.yaml .file .yaml}
store: xdg/data/dir/store.yaml
~~~

When we create two new CA keys, for host and user certification, they
both show up in the list.

~~~scenario
when I run sshca ca new user userCAv1
when I run sshca ca show userCAv1
then stdout contains "userCAv1"
then stdout contains "ssh-ed25519"

when I run sshca ca new host hostCAv1
when I run sshca ca show hostCAv1
then stdout contains "hostCAv1"
then stdout contains "ssh-ed25519"

when I run sshca ca list
then stdout is exactly "hostCAv1\nuserCAv1\n"
when I run sshca ca list --json
then stdout contains "hostCAv1"
then stdout doesn't contain "-----BEGIN OPENSSH PRIVATE KEY-----"
~~~

We can see the CA public key.

~~~scenario
when I run sshca ca public-key hostCAv1
then stdout matches regex ^ssh-ed25519\s\S+\s$
when I run sshca ca public-key userCAv1
then stdout matches regex ^ssh-ed25519\s\S+\s$
~~~

When we remove a CA key, it's no longer in the store.

~~~scenario
when I run sshca ca remove hostCAv1
when I run sshca ca list
then stdout contains "userCAv1"
when I run sshca ca remove userCAv1
when I run sshca ca list
then stdout is exactly ""
~~~

When we create two CA keys, they can be individually removed.

~~~scenario
when I run sshca ca new host CAv1
when I run sshca ca new host CAv2
when I run sshca ca list
then stdout contains "CAv1"
then stdout contains "CAv2"
when I run sshca ca remove CAv1
when I run sshca ca list
then stdout doesn't contain "CAv1"
then stdout contains "CAv2"
when I run sshca ca remove CAv2
when I run sshca ca list
then stdout is exactly ""
~~~

### Show CA in store

_Requirement: We must be able to inspect a CA in the store._

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml
when I run sshca ca new user sillyca
when I run sshca ca rename sillyca userCAv1
when I run sshca ca show userCAv1
then stdout contains "userCAv1"
when I try to run sshca ca show alfred
then command fails
then stderr contains "alfred"
~~~

### Rename CA in store

_Requirement: We must be able to renamme a CA in the store._

Justification: Sometimes it's necessary to change the name of a CA,
and it's convenient to be able to just rename them without having to
export and then re-import their data.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml
when I run sshca ca new user sillyca
when I run sshca ca rename sillyca userCAv1
when I run sshca ca list
then stdout contains "userCAv1"
then stdout doesn't contain "sillyca"
~~~

## Host management

The `sshca` tool needs to manage some information about hosts. At
minimum, the host's name and public key. However, `sshca` can also
generate a host key, and store the private key as well.


### Import host public key into store

_Requirement: It must be possible to import a host's public key into
the store._

First we verify there are no hosts in the store.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml
when I run sshca host list
then stdout is exactly ""
when I run sshca host list --json
then stdout contains "[]"
~~~

Then we create a host key pair and import the public key into the
store.

~~~scenario
when I run ssh-keygen -t ed25519 -N '' -f myhost
when I run sshca host new myhost.example.com myhost.pub

when I run sshca host show myhost.example.com
then stdout contains "myhost.example.com"
then stdout contains "ssh-ed25519"

when I run sshca host list
then stdout contains "myhost.example.com"

when I run sshca host list --json
then stdout contains "myhost.example.com"
then stdout doesn't contain "-----BEGIN OPENSSH PRIVATE KEY-----"
~~~

We must not be able to import another public key for the same host.

~~~scenario
when I run ssh-keygen -f myhost2 -N ""
when I try to run sshca host new myhost.example.com myhost2.pub
then command fails
then stderr contains "myhost.example.com"
when I run sshca host list
then stdout contains "myhost.example.com"
~~~


### Show host in store

_Requirement: We must be able to inspect a host in the store._

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml
when I run sshca host generate myhost
when I run sshca host show myhost
then stdout contains "myhost"
when I try to run sshca host show alfred
then command fails
then stderr contains "alfred"
~~~


### Add principals to a new host

_Requirement: We must be able to set any number of principals for a
host._

Justification: Many hosts have multiple name, such as `foo` and
`foo.example.com`.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml
when I run sshca host generate myhost --principal alfred.lan -p alfred.example.com

when I run sshca host principals list myhost
then stdout doesn't contain "myhost"
then stdout contains "alfred.lan"
then stdout contains "alfred.example.com"
~~~

### Manage principals of an existing host

_Requirement: It must be possible to change principals to an existing
host._

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml
when I run sshca host generate myhost

when I run sshca host principals list myhost
then stdout doesn't contain "alfred.lan"
then stdout doesn't contain "alfred.example.com"

when I run sshca host principals add --host myhost alfred.lan
when I run sshca host principals list myhost
then stdout contains "alfred.lan"
then stdout doesn't contain "alfred.example.com"

when I run sshca host principals add --host myhost alfred.example.com
when I run sshca host principals list myhost
then stdout contains "alfred.lan"
then stdout contains "alfred.example.com"
~~~


### Rename host in store

_Requirement: We must be able to renamme a host in the store._

Justification: Sometimes hosts change name, and it's convenient to be
able to just rename them without having to export and then re-import
their data.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml
when I run sshca host list
then stdout is exactly ""
when I run sshca host generate myhost
when I run sshca host rename myhost newhost
when I run sshca host list
then stdout contains "newhost"
then stdout doesn't contain "myhost"
~~~

### Remove host from store

_Requirement: We must be able to remove a host from the store._

Justification: We don't want the store to grow in size indefinitely.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml
when I run sshca host list
then stdout is exactly ""
when I run ssh-keygen -t ed25519 -N '' -f myhost
when I run sshca host new myhost.example.com myhost.pub
when I run sshca host remove myhost.example.com
when I run sshca host list
then stdout is exactly ""
~~~

### Generate a host key

_Requirement: add a host by generating a host key._

Justification: this enables a use case where host keys are installed
onto a system rather than generated on the system. This is useful, for
example, when first installing a system: the installation environment
may not be able to generate a good key, and certainly doesn't have the
CA private key to create a certificate.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml

when I run sshca host generate myhost.example.com
when I run sshca host list
then stdout contains "myhost.example.com"
when I run sshca host public-key myhost.example.com
then stdout contains "ssh-ed25519 "
when I run sshca host private-key myhost.example.com
then stdout contains "-----BEGIN OPENSSH PRIVATE KEY-----"
~~~

### Generate a short-lived host key

_Requirement: a host key can be marked as short-lived._

Justification: when a host key is generated for installing a new host,
it should be replaced soon. The `sshca` tool should allow marking the
installation host key as short-lived, so that it can't be used for
long.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml

when I run sshca host generate --temporary myhost.example.com
when I run sshca host list
then stdout contains "myhost.example.com"
when I run sshca host public-key myhost.example.com
then stdout contains "ssh-ed25519 "
when I try to run sshca host public-key --now=3000-01-01T00:00:00 myhost.example.com
then command fails
~~~

### Re-generate a host key

_Requirement: we can generate a new host key for an existing host that
already has a private key._

Justification: Sometimes it's good to create a new host key. It's
convenient if one doesn't need to remove the host first, and can keep
all other data about the host. However, re-generating a key should
only be possible for a host that has a private key (which means it was
generated with the sshca tool).

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml

when I run ssh-keygen -t ed25519 -N '' -f myhost
when I run sshca host new myhost myhost.pub

when I try to run sshca host generate myhost
then command fails

when I run sshca host remove myhost
when I try to run sshca host regenerate myhost
then command fails

when I run sshca host generate myhost
when I run sshca host regenerate myhost

when I run sshca host list
then stdout contains "myhost"
when I run sshca host public-key myhost
then stdout contains "ssh-ed25519 "
when I run sshca host private-key myhost
then stdout contains "-----BEGIN OPENSSH PRIVATE KEY-----"
~~~

### Re-generate a temporary host key

_Requirement: we can generate a new short-lived host key for an
existing host that already has a private key._

Justification: when a host key is generated for installing a new host,
it should be replaced soon. The `sshca` tool should allow marking the
installation host key as short-lived, so that it can't be used for
long.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml

when I run sshca host generate myhost
when I run sshca host regenerate --temporary myhost

when I try to run sshca host public-key --now=3000-01-01T00:00:00 myhost
then command fails
~~~

### Export host public and private keys

_Requirement: It must be possible to export a host's public key, and
also the private key when it's known._

Justification: Exporting the public key is handy for debugging.
Exporting the private key enables `sshca` to be the canonical source
of truth for a host's SSH identity, which is handy for setting the
identity via configuration management, and for initial system
installation before the first boot.

However, the private key can only be exported if `sshca` generated the
key. If a host public key was imported, there is no private key stored
for the host.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml

when I try to run sshca host public-key myhost.example.com
then command fails

when I try to run sshca host private-key myhost.example.com
then command fails

when I run ssh-keygen -t ed25519 -N '' -f myhost
when I run sshca host new myhost.example.com myhost.pub

when I run sshca host public-key myhost.example.com
then stdout contains "ssh-ed25519 "

when I try to run sshca host private-key myhost.example.com
then command fails

when I run sshca host generate otherhost.example.com
when I run sshca host public-key otherhost.example.com
then stdout contains "ssh-ed25519 "
when I run sshca host private-key otherhost.example.com
then stdout contains "-----BEGIN OPENSSH PRIVATE KEY-----"
~~~

### Certify a host

_Requirement: we must be able to create a host certificate._

Justification: creating certificates is the reason for the tool to
exist.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml

when I run sshca ca new host CAv1
when I run sshca host generate myhost -p myhost -p othername

when I run sshca host certify --ca CAv1 myhost
then stdout matches regex ^ssh-ed25519-cert-v01@openssh.com

when I run sshca host certify --output my.cert --ca CAv1 myhost
then file my.cert matches regex /^ssh-ed25519-cert-v01@openssh.com/

when I run ssh-keygen -L -f my.cert
then stdout contains "myhost"
then stdout contains "othername"
~~~

### Host CA can't certify a user

_Requirement: we can't certify a user with a host CA._

Justification: It must not be easy to make this mistake.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml

when I run ssh-keygen -t ed25519 -N '' -f some_user
when I run sshca user new some_user some_user.pub

when I run sshca ca new host CAv1
when I try to run sshca user certify --ca CAv1 some_user
then command fails
then stderr contains "unknown user CA: CAv1"
~~~

### By default host certificates are valid for 90 days

_Requirement: By default, host certificates should be valid a limited time._

Justification: This is a bit of a gut feeling rather than a proper
reason, but it doesn't seem useful for a certificate to be valid
forever, just like a TLS certificate from Let's Encrypt isn't.

Note that due to it being hard to parse, we just check that there is a
validity period set in the certificate, rather than checking it's 90
days.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml
when I run sshca ca new host CAv1
when I run sshca host generate myhost.example.com
when I run sshca host certify --output my.cert --ca CAv1 myhost.example.com
when I run ssh-keygen -L -f my.cert
then stdout matches regex Valid: from \d+-\d+-\d+T\d+:\d+:\d+ to \d+-\d+-\d+T\d+:\d+:\d+
~~~

### Host certificate validity can be set

_Requirement: We can create a host certificate with a limited validity
period._

Justification: Sometimes the default validity is too long, such as
when doing an initial install on a system, when the host private key
can't be secured strongly. We can work around that by having a very
short certificate lifetime for the initial install, and then install a
new host private key and longer-lived certificate after the system has
booted.

Note that due to it being hard to parse, we just check that there is a
validity period set in the certificate, rather than checking it's what
we specify.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml
when I run sshca ca new host CAv1
when I run sshca host generate myhost.example.com

when I run sshca host certify  --output my.cert --ca CAv1 myhost.example.com --expires-in 1d
when I run ssh-keygen -L -f my.cert
then stdout matches regex Valid: from \d+-\d+-\d+T\d+:\d+:\d+ to \d+-\d+-\d+T\d+:\d+:\d+
~~~

## User management

The `sshca` tool needs to manage some information about users: their
public key and username.


### Import user public key into store

_Requirement: It must be possible to import a user's public key into
the store._

First we verify there are no users in the store.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml
when I run sshca user list
then stdout is exactly ""
when I run sshca user list --json
then stdout contains "[]"
~~~

Then we create a user key pair and import the public key into the
store.

~~~scenario
when I run ssh-keygen -t ed25519 -N '' -f myself
when I run sshca user new myname myself.pub

when I run sshca user show myname
then stdout contains "myname"
then stdout contains "ssh-ed25519"
then stdout doesn't contain "private_key"

when I run sshca user list
then stdout contains "myname"

when I run sshca user list --json
then stdout contains "myname"
then stdout doesn't contain "-----BEGIN OPENSSH PRIVATE KEY-----"
~~~

We must not be able to import another public key for the same user.

~~~scenario
when I run ssh-keygen -N "" -f myself2
when I try to run sshca user new myname myself2.pub
then command fails
then stderr contains "myname"
when I run sshca user list
then stdout contains "myname"
~~~

### Show user in store

_Requirement: We must be able to inspect a user in the store._

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml
when I run ssh-keygen -t ed25519 -N '' -f bruce
when I run sshca user new bruce bruce.pub
when I run sshca user show bruce
then stdout contains "bruce"
when I try to run sshca user show alfred
then command fails
then stderr contains "alfred"
~~~


### Add principals to a new user

_Requirement: It must be possible to set any number of principals for
a new user._

Justification: the same user might need to log in as themselves, or into
various role accounts, such as `root`, in the realm of hosts that
trust a CA.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml
when I run ssh-keygen -t ed25519 -N '' -f tomjon
when I run sshca user new tomjon tomjon.pub --principal root -p king

when I run sshca user principals list tomjon
then stdout doesn't contain "tomjon"
then stdout contains "root"
then stdout contains "king"
~~~

### Manage principals of an existing user

_Requirement: It must be possible to change principals to an existing
user._

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml
when I run ssh-keygen -t ed25519 -N '' -f tomjon
when I run sshca user new tomjon tomjon.pub

when I run sshca user principals list tomjon
then stdout doesn't contain "root"
then stdout doesn't contain "king"

when I run sshca user principals add --user tomjon root king
when I run sshca user principals list tomjon
then stdout contains "root"
then stdout contains "king"

when I run sshca user principals remove --user tomjon king
when I run sshca user principals list tomjon
then stdout contains "root"
then stdout doesn't contain "king"


~~~


### Rename user in store

_Requirement: We must be able to rename a user in the store._

Justification: Sometimes the user changes their name and so the
username changes. It's convenient to be able to rename them, instead
of exporting the public key, removing the user, and adding them back
with a new username.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml
when I run ssh-keygen -t ed25519 -N '' -f myself
when I run sshca user new myname myself.pub
when I run sshca user rename myname newname
when I run sshca user list
then stdout contains "newname"
then stdout doesn't contain "myname"
~~~


### Remove user from store

_Requirement: We must be able to remove a user from the store._

Justification: We don't want the store to grow in size indefinitely.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml
when I run ssh-keygen -t ed25519 -N '' -f myself
when I run sshca user new myname myself.pub
when I run sshca user remove myname
when I run sshca user list
then stdout is exactly ""
~~~

### Export user public key

_Requirement: It must be possible to export a host's public key._

Justification: Exporting the public key is handy for debugging.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml

when I try to run sshca user public-key myname
then command fails

when I run ssh-keygen -t ed25519 -N '' -f myself
when I run sshca user new myname myself.pub

when I run sshca user public-key myname
then stdout contains "ssh-ed25519 "
~~~

### Certify a user

_Requirement: we must be able to create a user certificate._

Justification: creating certificates is the reason for the tool to
exist.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml

when I run sshca ca new user CAv1
when I run ssh-keygen -t ed25519 -N '' -f myself
when I run sshca user new myname myself.pub --principal tomjon -p king

when I run sshca user certify --ca CAv1 myname
then stdout matches regex ^ssh-ed25519-cert-v01@openssh.com

when I run sshca user certify --output my.cert --ca CAv1 myname
then file my.cert matches regex /^ssh-ed25519-cert-v01@openssh.com/

when I run ssh-keygen -Lf my.cert
then stdout doesn't match regex ^\s+myname$
then stdout matches regex ^\s+tomjon
then stdout matches regex ^\s+king
~~~

### By default user certificates are valid for 90 days

_Requirement: By default, user certificates should be valid a limited time._

Justification: We have not reliable way of revoking a certificate, or,
rather, distributing the revocation to all hosts that may trust our
CA, unless they are all under own control. However, creating a new
certificate is easy and only needs to be distributed to to the user.

Note that due to it being hard to parse, we just check that there is a
validity period set in the certificate, rather than checking it's 90
days.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml
when I run sshca ca new user CAv1
when I run ssh-keygen -t ed25519 -N '' -f myself
when I run sshca user new myname myself.pub
when I run sshca user certify --output my.cert --ca CAv1 myname
when I run ssh-keygen -L -f my.cert
then stdout matches regex Valid: from \d+-\d+-\d+T\d+:\d+:\d+ to \d+-\d+-\d+T\d+:\d+:\d+
~~~

### User certificate validity can be set

_Requirement: We can create a user certificate with a limited validity
period._

Justification: Sometimes the default validity is too long, such as
when we want to limit the time window in which a compromised user
private key may be used by an attacker.

Note that due to it being hard to parse, we just check that there is a
validity period set in the certificate, rather than checking it's what
we specify.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml
when I run sshca ca new user CAv1
when I run ssh-keygen -t ed25519 -N '' -f myself
when I run sshca user new myname myself.pub
when I run sshca user certify --output my.cert --ca CAv1 myname --expires-in 1d
when I run ssh-keygen -L -f my.cert
then stdout matches regex Valid: from \d+-\d+-\d+T\d+:\d+:\d+ to \d+-\d+-\d+T\d+:\d+:\d+
~~~

### Certify all users

_Requirement: we must be able to create all users certificates at once._

Justification: This is convenient if there are many users.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml

when I run sshca ca new user CAv1

when I run ssh-keygen -t ed25519 -N '' -f granny
when I run sshca user new granny granny.pub

when I run ssh-keygen -t ed25519 -N '' -f gytha
when I run sshca user new gytha gytha.pub

when I run sshca user certify --ca CAv1 --all
then file granny-cert.pub matches regex /^ssh-ed25519-cert-v01@openssh.com/
then file gytha-cert.pub matches regex /^ssh-ed25519-cert-v01@openssh.com/

given a directory certs
when I run sshca user certify --ca CAv1 --all --output=certs
then file certs/granny-cert.pub matches regex /^ssh-ed25519-cert-v01@openssh.com/
then file certs/gytha-cert.pub matches regex /^ssh-ed25519-cert-v01@openssh.com/
~~~

### User CA can't certify a host

_Requirement: we can't certify a host with a user CA._

Justification: It must not be easy to make this mistake.

~~~scenario
given an installed sshca
given file .config/sshca/config.yaml from config.yaml

when I run sshca host generate myhost -p myhost -p othername

when I run sshca ca new user CAv1
when I try to run sshca host certify --ca CAv1 myhost
then command fails
then stderr contains "unknown host CA: CAv1"
~~~
# SEE ALSO

* [ssh-keygen manual page](https://www.man7.org/linux/man-pages/man1/ssh-keygen.1.html#CERTIFICATES)

# Thanks

While writing this, the author got feedback and reviews of drafts
from David Leggett.