Skip to content

Commit

Permalink
Be able to load KMS-encrypted keys
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
bobveznat committed Apr 4, 2016
1 parent 07e7010 commit d2a532c
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 5 deletions.
84 changes: 80 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ Effectively the format is::
MaxCertLifetime
SigningKeyFingerprint
PrivateKeyFile
KmsRegion
AuthorizedSigners {
<key fingerprint>: <key identity>
}
Expand All @@ -294,10 +295,18 @@ Effectively the format is::
sign complete requests. This should be the fingerprint of your CA. When using
this option you must, somehow, load the private key into the agent such that
the daemon can use it.
- ``PrivateKeyFile``: A path to a private key file. As of this writing the key
must be unencrypted. Do take explicit care if you're using unencrypted
private keys. The next release / commit will include support for private key
files that are encrypted using Amazon's KMS.
- ``PrivateKeyFile``: A path to a private key file. The key may be
unencrypted or have previously been encrypted using Amazon's KMS. If
the key was encrypted using KMS simply name it with a ".kms" extension
and ssh-cert-authority will attempt to decrypt the key on startup. See
the section on Encrypting a CA Key for help in using KMS to encrypt
the key.
- ``KmsRegion``: If sign_certd encounters a privatekey file with an
extension of ".kms" it will attempt to decrypt it using KMS in the
same region that the software is running in. It determines this using
the local instance's metadata server. If you're not running
ssh-cert-authority within AWS or if the key is in a different region
you'll need to specify the region here as a string, e.g. us-west-2.
- ``AuthorizedSigners``: A hash keyed by key fingerprints and values of key
ids. I recommend this be set to a username. It will appear in the
resultant SSH certificate in the KeyId field as well in
Expand Down Expand Up @@ -336,6 +345,73 @@ You can take that value and add in your keys like so::

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

Encrypting a CA Key Using Amazon's KMS
======================================

Amazon's KMS (Key Management Service) provides an encryption key
management service that can be used to encrypt small chunks of arbitrary
data (including other keys). This project supports using KMS to keep the
CA key secure.

The recommended deployment is to launch ssh-cert-authority onto an EC2
instance that has an EC2 instance profile attached to it that allows it
to use KMS to decrypt the CA key. A sample cloudformation stack is
forthcoming to do all of this on your behalf.

Create Instance Profile
```````````````````````

In the mean time you can set things up by hand. A sample EC2 instance
profile access policy::

{
"Statement": [
{
"Resource": [
"*"
],
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Effect": "Allow"
}
],
"Version": "2012-10-17"
}

Create KMS Key
``````````````

Create a KMS key in the AWS IAM console. When specifying key usage allow the
instance profile you created earlier to use the key. The key you create
will have an id associated with it, it looks something like this:

arn:aws:kms:us-west-2:123412341234:key/debae348-3666-4cc7-9d25-41e33edb2909

Save that for the next step.

Launch Instance
```````````````

Now launch an instance and use the EC2 instance profile. A t2 class instance is
likely sufficient. Copy over the latest ssh-cert-authority binary (you
can also use the container) and generate a new key for the CA using
ssh-keygen and then use ssh-cert-authority to encrypt it::

environment_name=production
ssh-keygen -q -t rsa -b 4096 -C "ssh-cert-authority ${environment_name}" -f ca-key-${environment_name}
cat ca-key-${environment_name} | ./ssh-cert-authority-linux-amd64 encrypt-key --key-id \
arn:aws:kms:us-west-2:881577346222:key/d1401480-8220-4bb7-a1de-d03dfda44a13 \
--output ca-key-${environment_name}.kms && rm ca-key-${environment_name}

At this point you're ready to fire up the authority. The rest of this
document applies, simply add a PrivateKeyFile option to signer certd's
config for the environment you're working on and reference the path to
the encrypted file we just created, `ca-key-${environment_name}.kms`

Requesting Certificates
=======================
Expand Down
53 changes: 53 additions & 0 deletions encrypt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package main

import (
"bufio"
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/kms"
"github.com/codegangsta/cli"
"io/ioutil"
"os"
)

func encryptFlags() []cli.Flag {
return []cli.Flag{
cli.StringFlag{
Name: "key-id",
Value: "",
Usage: "The ARN of the KMS key to use",
},
cli.StringFlag{
Name: "output",
Value: "ca-key.kms",
Usage: "The filename for key output",
},
}
}

func encryptKey(c *cli.Context) {
region, err := ec2metadata.New(session.New(), aws.NewConfig()).Region()
if err != nil {
fmt.Printf("Unable to determine our region: %s", err)
os.Exit(1)
}
keyContents, err := ioutil.ReadAll(bufio.NewReader(os.Stdin))
if err != nil {
fmt.Printf("Unable to read private key: %s", err)
os.Exit(1)
}
svc := kms.New(session.New(), aws.NewConfig().WithRegion(region))
params := &kms.EncryptInput{
Plaintext: keyContents,
KeyId: aws.String(c.String("key-id")),
}
resp, err := svc.Encrypt(params)
if err != nil {
fmt.Printf("Unable to Encrypt CA key: %v\n", err)
os.Exit(1)
}
keyContents = resp.CiphertextBlob
ioutil.WriteFile(c.String("output"), resp.CiphertextBlob, 0444)
}
6 changes: 6 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ func main() {
Usage: "Run the cert-authority web service",
Action: signCertd,
},
{
Name: "encrypt-key",
Flags: encryptFlags(),
Usage: "Encrypt an ssh private key from stdin",
Action: encryptKey,
},
}
app.Run(os.Args)
}
33 changes: 32 additions & 1 deletion sign_certd.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/kms"
"github.com/cloudtools/ssh-cert-authority/client"
"github.com/cloudtools/ssh-cert-authority/util"
"github.com/codegangsta/cli"
Expand Down Expand Up @@ -107,6 +111,29 @@ func (h *certRequestHandler) setupPrivateKeys(config map[string]ssh_ca_util.Sign
if err != nil {
return fmt.Errorf("Failed reading private key file %s: %v", cfg.PrivateKeyFile, err)
}
if strings.HasSuffix(cfg.PrivateKeyFile, ".kms") {
var region string
if cfg.KmsRegion != "" {
region = cfg.KmsRegion
} else {
region, err = ec2metadata.New(session.New(), aws.NewConfig()).Region()
if err != nil {
return fmt.Errorf("Unable to determine our region: %s", err)
}
}
svc := kms.New(session.New(), aws.NewConfig().WithRegion(region))
params := &kms.DecryptInput{
CiphertextBlob: keyContents,
}
resp, err := svc.Decrypt(params)
if err != nil {
// We try only one time to speak with KMS. If this pukes, and it
// will occasionally because "the cloud", the caller is responsible
// for trying again, possibly after a crash/restart.
return fmt.Errorf("Unable to decrypt CA key: %v\n", err)
}
keyContents = resp.Plaintext
}
key, err := ssh.ParseRawPrivateKey(keyContents)
if err != nil {
return fmt.Errorf("Failed parsing private key %s: %v", cfg.PrivateKeyFile, err)
Expand Down Expand Up @@ -585,7 +612,11 @@ func runSignCertd(config map[string]ssh_ca_util.SignerdConfig) {
}
requestHandler := makeCertRequestHandler(config)
requestHandler.sshAgentConn = sshAgentConn
requestHandler.setupPrivateKeys(config)
err = requestHandler.setupPrivateKeys(config)
if err != nil {
log.Println("Failed CA key load: %v\n", err)
os.Exit(1)
}

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

Expand Down
1 change: 1 addition & 0 deletions util/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type SignerdConfig struct {
SlackChannel string
MaxCertLifetime int
PrivateKeyFile string
KmsRegion string
}

type SignerConfig struct {
Expand Down

0 comments on commit d2a532c

Please sign in to comment.