Skip to content

Commit d2a532c

Browse files
committed
Be able to load KMS-encrypted keys
This commit introduces KMS functionality. Given an encrypted private key for the cert authority the signing daemon will call out to KMS on startup to decrypt the key and load it into the ssh-agent. Docs were updated accordingly.
1 parent 07e7010 commit d2a532c

File tree

5 files changed

+172
-5
lines changed

5 files changed

+172
-5
lines changed

README.rst

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ Effectively the format is::
273273
MaxCertLifetime
274274
SigningKeyFingerprint
275275
PrivateKeyFile
276+
KmsRegion
276277
AuthorizedSigners {
277278
<key fingerprint>: <key identity>
278279
}
@@ -294,10 +295,18 @@ Effectively the format is::
294295
sign complete requests. This should be the fingerprint of your CA. When using
295296
this option you must, somehow, load the private key into the agent such that
296297
the daemon can use it.
297-
- ``PrivateKeyFile``: A path to a private key file. As of this writing the key
298-
must be unencrypted. Do take explicit care if you're using unencrypted
299-
private keys. The next release / commit will include support for private key
300-
files that are encrypted using Amazon's KMS.
298+
- ``PrivateKeyFile``: A path to a private key file. The key may be
299+
unencrypted or have previously been encrypted using Amazon's KMS. If
300+
the key was encrypted using KMS simply name it with a ".kms" extension
301+
and ssh-cert-authority will attempt to decrypt the key on startup. See
302+
the section on Encrypting a CA Key for help in using KMS to encrypt
303+
the key.
304+
- ``KmsRegion``: If sign_certd encounters a privatekey file with an
305+
extension of ".kms" it will attempt to decrypt it using KMS in the
306+
same region that the software is running in. It determines this using
307+
the local instance's metadata server. If you're not running
308+
ssh-cert-authority within AWS or if the key is in a different region
309+
you'll need to specify the region here as a string, e.g. us-west-2.
301310
- ``AuthorizedSigners``: A hash keyed by key fingerprints and values of key
302311
ids. I recommend this be set to a username. It will appear in the
303312
resultant SSH certificate in the KeyId field as well in
@@ -336,6 +345,73 @@ You can take that value and add in your keys like so::
336345

337346
Once the server is up and running it is bound to 0.0.0.0 on port 8080.
338347

348+
Encrypting a CA Key Using Amazon's KMS
349+
======================================
350+
351+
Amazon's KMS (Key Management Service) provides an encryption key
352+
management service that can be used to encrypt small chunks of arbitrary
353+
data (including other keys). This project supports using KMS to keep the
354+
CA key secure.
355+
356+
The recommended deployment is to launch ssh-cert-authority onto an EC2
357+
instance that has an EC2 instance profile attached to it that allows it
358+
to use KMS to decrypt the CA key. A sample cloudformation stack is
359+
forthcoming to do all of this on your behalf.
360+
361+
Create Instance Profile
362+
```````````````````````
363+
364+
In the mean time you can set things up by hand. A sample EC2 instance
365+
profile access policy::
366+
367+
{
368+
"Statement": [
369+
{
370+
"Resource": [
371+
"*"
372+
],
373+
"Action": [
374+
"kms:Encrypt",
375+
"kms:Decrypt",
376+
"kms:ReEncrypt",
377+
"kms:GenerateDataKey",
378+
"kms:DescribeKey"
379+
],
380+
"Effect": "Allow"
381+
}
382+
],
383+
"Version": "2012-10-17"
384+
}
385+
386+
Create KMS Key
387+
``````````````
388+
389+
Create a KMS key in the AWS IAM console. When specifying key usage allow the
390+
instance profile you created earlier to use the key. The key you create
391+
will have an id associated with it, it looks something like this:
392+
393+
arn:aws:kms:us-west-2:123412341234:key/debae348-3666-4cc7-9d25-41e33edb2909
394+
395+
Save that for the next step.
396+
397+
Launch Instance
398+
```````````````
399+
400+
Now launch an instance and use the EC2 instance profile. A t2 class instance is
401+
likely sufficient. Copy over the latest ssh-cert-authority binary (you
402+
can also use the container) and generate a new key for the CA using
403+
ssh-keygen and then use ssh-cert-authority to encrypt it::
404+
405+
environment_name=production
406+
ssh-keygen -q -t rsa -b 4096 -C "ssh-cert-authority ${environment_name}" -f ca-key-${environment_name}
407+
cat ca-key-${environment_name} | ./ssh-cert-authority-linux-amd64 encrypt-key --key-id \
408+
arn:aws:kms:us-west-2:881577346222:key/d1401480-8220-4bb7-a1de-d03dfda44a13 \
409+
--output ca-key-${environment_name}.kms && rm ca-key-${environment_name}
410+
411+
At this point you're ready to fire up the authority. The rest of this
412+
document applies, simply add a PrivateKeyFile option to signer certd's
413+
config for the environment you're working on and reference the path to
414+
the encrypted file we just created, `ca-key-${environment_name}.kms`
339415

340416
Requesting Certificates
341417
=======================

encrypt.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"github.com/aws/aws-sdk-go/aws"
7+
"github.com/aws/aws-sdk-go/aws/ec2metadata"
8+
"github.com/aws/aws-sdk-go/aws/session"
9+
"github.com/aws/aws-sdk-go/service/kms"
10+
"github.com/codegangsta/cli"
11+
"io/ioutil"
12+
"os"
13+
)
14+
15+
func encryptFlags() []cli.Flag {
16+
return []cli.Flag{
17+
cli.StringFlag{
18+
Name: "key-id",
19+
Value: "",
20+
Usage: "The ARN of the KMS key to use",
21+
},
22+
cli.StringFlag{
23+
Name: "output",
24+
Value: "ca-key.kms",
25+
Usage: "The filename for key output",
26+
},
27+
}
28+
}
29+
30+
func encryptKey(c *cli.Context) {
31+
region, err := ec2metadata.New(session.New(), aws.NewConfig()).Region()
32+
if err != nil {
33+
fmt.Printf("Unable to determine our region: %s", err)
34+
os.Exit(1)
35+
}
36+
keyContents, err := ioutil.ReadAll(bufio.NewReader(os.Stdin))
37+
if err != nil {
38+
fmt.Printf("Unable to read private key: %s", err)
39+
os.Exit(1)
40+
}
41+
svc := kms.New(session.New(), aws.NewConfig().WithRegion(region))
42+
params := &kms.EncryptInput{
43+
Plaintext: keyContents,
44+
KeyId: aws.String(c.String("key-id")),
45+
}
46+
resp, err := svc.Encrypt(params)
47+
if err != nil {
48+
fmt.Printf("Unable to Encrypt CA key: %v\n", err)
49+
os.Exit(1)
50+
}
51+
keyContents = resp.CiphertextBlob
52+
ioutil.WriteFile(c.String("output"), resp.CiphertextBlob, 0444)
53+
}

main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ func main() {
4747
Usage: "Run the cert-authority web service",
4848
Action: signCertd,
4949
},
50+
{
51+
Name: "encrypt-key",
52+
Flags: encryptFlags(),
53+
Usage: "Encrypt an ssh private key from stdin",
54+
Action: encryptKey,
55+
},
5056
}
5157
app.Run(os.Args)
5258
}

sign_certd.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import (
88
"encoding/json"
99
"errors"
1010
"fmt"
11+
"github.com/aws/aws-sdk-go/aws"
12+
"github.com/aws/aws-sdk-go/aws/ec2metadata"
13+
"github.com/aws/aws-sdk-go/aws/session"
14+
"github.com/aws/aws-sdk-go/service/kms"
1115
"github.com/cloudtools/ssh-cert-authority/client"
1216
"github.com/cloudtools/ssh-cert-authority/util"
1317
"github.com/codegangsta/cli"
@@ -107,6 +111,29 @@ func (h *certRequestHandler) setupPrivateKeys(config map[string]ssh_ca_util.Sign
107111
if err != nil {
108112
return fmt.Errorf("Failed reading private key file %s: %v", cfg.PrivateKeyFile, err)
109113
}
114+
if strings.HasSuffix(cfg.PrivateKeyFile, ".kms") {
115+
var region string
116+
if cfg.KmsRegion != "" {
117+
region = cfg.KmsRegion
118+
} else {
119+
region, err = ec2metadata.New(session.New(), aws.NewConfig()).Region()
120+
if err != nil {
121+
return fmt.Errorf("Unable to determine our region: %s", err)
122+
}
123+
}
124+
svc := kms.New(session.New(), aws.NewConfig().WithRegion(region))
125+
params := &kms.DecryptInput{
126+
CiphertextBlob: keyContents,
127+
}
128+
resp, err := svc.Decrypt(params)
129+
if err != nil {
130+
// We try only one time to speak with KMS. If this pukes, and it
131+
// will occasionally because "the cloud", the caller is responsible
132+
// for trying again, possibly after a crash/restart.
133+
return fmt.Errorf("Unable to decrypt CA key: %v\n", err)
134+
}
135+
keyContents = resp.Plaintext
136+
}
110137
key, err := ssh.ParseRawPrivateKey(keyContents)
111138
if err != nil {
112139
return fmt.Errorf("Failed parsing private key %s: %v", cfg.PrivateKeyFile, err)
@@ -585,7 +612,11 @@ func runSignCertd(config map[string]ssh_ca_util.SignerdConfig) {
585612
}
586613
requestHandler := makeCertRequestHandler(config)
587614
requestHandler.sshAgentConn = sshAgentConn
588-
requestHandler.setupPrivateKeys(config)
615+
err = requestHandler.setupPrivateKeys(config)
616+
if err != nil {
617+
log.Println("Failed CA key load: %v\n", err)
618+
os.Exit(1)
619+
}
589620

590621
log.Println("Server started with config", config)
591622

util/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type SignerdConfig struct {
2121
SlackChannel string
2222
MaxCertLifetime int
2323
PrivateKeyFile string
24+
KmsRegion string
2425
}
2526

2627
type SignerConfig struct {

0 commit comments

Comments
 (0)