Skip to content

Commit

Permalink
iam-request-ssh-key-signature: Added ssh agent support and default ke…
Browse files Browse the repository at this point in the history
…ys loading
  • Loading branch information
hamstah committed Jul 25, 2019
1 parent baed09c commit 12bfd5b
Show file tree
Hide file tree
Showing 6 changed files with 239 additions and 16 deletions.
100 changes: 97 additions & 3 deletions iam/request-ssh-key-signature/README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
# iam-request-ssh-key-signature

```
usage: iam-request-ssh-key-signature --lambda-arn=LAMBDA-ARN --ssh-public-key-filename=SSH-PUBLIC-KEY-FILENAME [<flags>]
usage: iam-request-ssh-key-signature --lambda-arn=LAMBDA-ARN [<flags>]
Request a signature for a SSH key from lambda-sign-ssh-key.
Flags:
--help Show context-sensitive help (also try --help-long and --help-man).
--lambda-arn=LAMBDA-ARN ARN of the lambda function signing the SSH key.
--ssh-private-key-filename=SSH-PRIVATE-KEY-FILENAME
Path to the SSH key to add to the agent.
--ssh-public-key-filename=SSH-PUBLIC-KEY-FILENAME
Path to the SSH key to sign.
--environment="" Name of the environment to sign the key for.
--duration=1m Duration of validity of the signature.
--dump Dump the event JSON instead of calling lambda
--dump Dump the event JSON instead of calling lambda.
--output=agent Where to store the generated certificate.
--source-address=SOURCE-ADDRESS ...
Set the IP restriction on the cert in CIDR format, can be repeated
Set the IP restriction on the cert in CIDR format, can be repeated.
--proxy-config=PROXY-CONFIG
Configuration for the ssh ProxyCommand host:port.
--assume-role-arn=ASSUME-ROLE-ARN
Role to assume
--assume-role-external-id=ASSUME-ROLE-EXTERNAL-ID
Expand All @@ -31,3 +36,92 @@ Flags:
--log-level=warn Log level
--log-format=text Log format
```

## Setup

Create the [lambda-sign-ssh-key function](../../lamnda/sign-ssh-key) Lambda with the associate config.

## Usage

### Default keys and ssh agent

The simplest usage is to sign the default SSH keys, the only required arguments are the Lambda ARN and the environment name:

```
$ iam-request-ssh-key-signature --environment prod \
--lambda-arn arn:aws:lambda:eu-west-1:123456789012:function:sign-ssh-key
```

If all works the command won't print any output and the keys should be in the ssh agent. You can check the certificate is there with

```
$ ssh-add -L | ssh-keygen -L -f -
(stdin):1 is not a certificate
(stdin):2:
Type: [email protected] user certificate
Public key: RSA-CERT SHA256:CW8pTPIANte2HWEt+bwUs/DoH46utEEXQ5vUV60nMVg
Signing CA: RSA SHA256:Bg3ycPpoLNi/NTT2nxgG5g5KMrO8ELMV0IBgnUZwACM
Key ID: "arn:aws:iam::123456789012:user/hamstah/29c50c35-c5aa-67e7-c956-ca3bf2949f3b"
Serial: 6878956844034323525
Valid: from 2019-07-25T01:35:33 to 2019-07-25T01:37:33
Principals:
hamstah
Critical Options:
source-address 127.0.0.1/32,172.17.0.0/16
Extensions:
permit-pty
```

You can see the default values from the Lambda config are used for the source address and expiry.

### Store the certificate outside the ssh agent

Change the `--output` option to `stdout`

```
$ iam-request-ssh-key-signature \
--environment prod \
--lambda-arn arn:aws:lambda:eu-west-1:123456789012:function:sign-ssh-key \
--output stdout | jq .
{
"certificate": "[email protected] AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgDCTmcNmWGQ+BSjhtSKYc4QyE8q1piiXAICHm16VLm1wAAAADAQABAAABAQC+agr/LBc2jrHjMdfmCnL7gy/zSB3WmIcpdCwUbadSRJc+ceLttVqoib7wf9VT5HqSWqlxdF0n9Ihh98jxwJZryM8LRM/IcW0LKtFKGJaSOtW6E0X6+G45TpCzyy2R5Vz7xf4zaZ3i784bDuUsjtimfGA3JYP8enfMmCHDXSORA/wL2mEyQsiPi7Bo+lom/qg8CGGlSqv+/S1yydL7F07nmYMytIVpby3Xv5355RuDEE2f+endrskv/QALyJzwBIjhZHD0ed5TNyNehPa89kTk9TblqZUPttBTu8fOHnihozejRoZp3jjDGR0jD8Nkvh3T46ACyuQcZXjWShQHvUFJtuqM+IzOAQwAAAABAAAAS2Fybjphd3M6aWFtOjozMzA0Mjg5MTM2ODM6dXNlci9oYW1zdGFoLzc2Mzk5OWM1LTU1NjUtMjk0Ni1jMTRjLWExMGE3NzVhNjA4ZQAAAAsAAAAHaGFtc3RhaAAAAABdOOwFAAAAAF047H0AAABCAAAADnNvdXJjZS1hZGRyZXNzAAAALAAAACgxMjcuMC4wLjEvMzIsMTcyLjE3LjAuMC8xNiw5MS4xMjYuMC4wLzE2AAAAEgAAAApwZXJtaXQtcHR5AAAAAAAAAAAAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQDBrCsmu3dTNJVHL+/3VVWYBnvgUTvanLrmuG7kCYrMx16AUGFIeoUVV9ulzu+M+DPqGb9pNBihAENpFm4Zfo2e3pPqkP3mpqfOkfdLxpw72zsadKhTClJue0Kd45d/0aPDFB1S6Ok9O0RCmy5720CHGdFY0ocRRpfQaGakI8+xQxuJ26FYAGns0VmoQFv7SZII/WDV00OotWo4908Qc+OMKbwaWH5c3gpzfID6x+XUG/+wesK5WmJ6mVWeGFfHhbugLdfWcXcNfsHVrL/KdUEQRjCMXpy7EUSAnf2WwEnLaGH42G9jeVcUBNED/aNI9QG7sl4YY5bDkgbU3bHKuRN1AAABDwAAAAdzc2gtcnNhAAABAHFMtUjn/DfdR9Hm8sNd77lIZqXz45zqnDZbwMGEWUiNz5js33FK0YmdkO0RwZb6/18X0JyO8jUZF6Cqwc63pkii65WRQpmHQfYWgT2BoL6G6A5h8XzvqYNX5yDUtwLp3wp0wuKuFU0o0u08jCpaoOsdQ53dJTchaz13OwYFgCCr8I8Smvl1BoRw0Mlb2/vxxA99mtAKagmgy3efey1l+cjctLS7dpEVp64T/mREM/d37ookgbqa04Kqc/brM/tty7s9rkeO5g53WWrSL4v7Kb5jTEFKj1hHBSjsFiVAHZ66xnyoFGVzGLZ3glWZ15wQAfLby71VB+9p4KJFpyBePAE=\n",
"duration": 60,
"valid_before": "2019-07-24T23:40:45Z"
}
```

The certificate can be extracted by piping the command to `jq .certificate > cert.pem` then connect with `-i cert.pem`.

### Overwrite source addresses

Use `--source-address` with a CIDR to restrict the source addresses to a specific address. The option can be repeated to
have more than one source addresses. This is useful for jumping through a bastion with an external IP for the
bastion and an internal one for the internal servers.

The CIDR must be a subset of the source addresses set in the environment configuration in the Lambda.

### Overwrite the session duration

Use `--duration` with the duration to use. The value must be shorter than the default duration in the environment configuration in the Lambda.


### Using ProxyCommand

To request the certificate as you connect the command can be used as an SSH ProxyCommand

```
$ ssh -o ProxyCommand="iam-request-ssh-key-signature --environment prod --lambda-arn arn:aws:lambda:eu-west-1:330428913683:function:sign-ssh-key --proxy-config %h:%p" [email protected]
```

Or add the `ProxyCommand` to `~/.ssh/config`

```
Host server.com
ProxyCommand iam-request-ssh-key-signature --environment prod --lambda-arn arn:aws:lambda:eu-west-1:330428913683:function:sign-ssh-key --proxy-config %h:%p
```


## View the certificates

By default the certificates are stored in the ssh-agent, you can view them with `ssh-add -L | ssh-keygen -L -f -`. Note that the
certificates are automatically removed from the ssh agent when expired.
136 changes: 125 additions & 11 deletions iam/request-ssh-key-signature/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,44 @@ package main
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net"
"os"
"time"

"github.com/aws/aws-sdk-go/service/lambda"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/hamstah/awstools/common"
"github.com/mitchellh/go-homedir"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)

var (
lambdaARN = kingpin.Flag("lambda-arn", "ARN of the lambda function signing the SSH key.").Required().String()
sshPublicKeyFilename = kingpin.Flag("ssh-public-key-filename", "Path to the SSH key to sign.").Required().String()
environment = kingpin.Flag("environment", "Name of the environment to sign the key for.").Default("").String()
duration = kingpin.Flag("duration", "Duration of validity of the signature.").Default("1m").Duration()
dump = kingpin.Flag("dump", "Dump the event JSON instead of calling lambda").Default("false").Bool()
sourceAddresses = kingpin.Flag("source-address", "Set the IP restriction on the cert in CIDR format, can be repeated").Strings()
lambdaARN = kingpin.Flag("lambda-arn", "ARN of the lambda function signing the SSH key.").Required().String()
sshPrivateKeyFilename = kingpin.Flag("ssh-private-key-filename", "Path to the SSH key to add to the agent.").String()
sshPublicKeyFilename = kingpin.Flag("ssh-public-key-filename", "Path to the SSH key to sign.").String()
environment = kingpin.Flag("environment", "Name of the environment to sign the key for.").Default("").String()
duration = kingpin.Flag("duration", "Duration of validity of the signature.").Default("1m").Duration()
dump = kingpin.Flag("dump", "Dump the event JSON instead of calling lambda.").Default("false").Bool()
output = kingpin.Flag("output", "Where to store the generated certificate.").Default("agent").Enum("agent", "stdout")
sourceAddresses = kingpin.Flag("source-address", "Set the IP restriction on the cert in CIDR format, can be repeated.").Strings()
proxyConfig = kingpin.Flag("proxy-config", "Configuration for the ssh ProxyCommand host:port.").String()
)

var (
defaultSSHKeyLocations = []string{"~/.ssh/id_ecdsa", "~/.ssh/id_rsa"}
)

type SignSSHKeyResponse struct {
Certificate string `json:"certificate"`
Duration int `json:"duration"`
ValidBefore time.Time `json:"valid_before"`
}

type LambdaPayload struct {
IdentityURL string `json:"identity_url"`
Environment string `json:"environment"`
Expand All @@ -30,19 +49,66 @@ type LambdaPayload struct {
SourceAddresses []string `json:"source_addresses"`
}

func HandleOptionalArgs() {
if len(*sshPrivateKeyFilename) == 0 {
found := false
for _, defaultSSHKeyLocation := range defaultSSHKeyLocations {
detected, err := homedir.Expand(defaultSSHKeyLocation)
if err != nil {
continue
}

if _, err := os.Stat(detected); err != nil {
continue
}

*sshPrivateKeyFilename = detected
found = true
break
}
if !found {
common.Fatalln("could not find the default private SSH key")
}
log.Info("Using default SSH key", *sshPrivateKeyFilename)
}
if len(*sshPublicKeyFilename) == 0 {
*sshPublicKeyFilename = fmt.Sprintf("%s.pub", *sshPrivateKeyFilename)
}
}

func ConnectIO(con net.Conn) {
c := make(chan int64)

copy := func(r io.ReadCloser, w io.WriteCloser) {
defer func() {
r.Close()
w.Close()
}()
n, _ := io.Copy(w, r)
c <- n
}

go copy(con, os.Stdout)
go copy(os.Stdin, con)

<-c
<-c
}

func main() {
kingpin.CommandLine.Name = "iam-request-ssh-key-signature"
kingpin.CommandLine.Help = "Request a signature for a SSH key from lambda-sign-ssh-key."
flags := common.HandleFlags()
HandleOptionalArgs()

sshPublicKeyBytes, err := ioutil.ReadFile(*sshPublicKeyFilename)
common.FatalOnError(err)
common.FatalOnErrorW(err, "could not read the SSH public key")

userSession := common.NewSession("")
stsClient := sts.New(userSession)

url, err := common.STSGetIdentityURL(stsClient)
common.FatalOnError(err)
common.FatalOnErrorW(err, "could not generate the STS signed URL")

session, conf := common.OpenSession(flags)

Expand All @@ -54,7 +120,7 @@ func main() {
SourceAddresses: *sourceAddresses,
}
lambdaPayloadBytes, err := json.Marshal(lambdaPayload)
common.FatalOnError(err)
common.FatalOnErrorW(err, "could not encode the lambda payload")

if *dump {
fmt.Println(string(lambdaPayloadBytes))
Expand All @@ -67,6 +133,54 @@ func main() {
FunctionName: lambdaARN,
Payload: lambdaPayloadBytes,
})
common.FatalOnError(err)
fmt.Println(string(ret.Payload))
common.FatalOnErrorW(err, "could not invoke the lambda function")

response := SignSSHKeyResponse{}
err = json.Unmarshal(ret.Payload, &response)
common.FatalOnErrorW(err, "could not parse the signature response")

switch *output {
case "agent":
parsedCert, _, _, _, err := ssh.ParseAuthorizedKey([]byte(response.Certificate))
common.FatalOnErrorW(err, "could not parse the certificate from the response")

cert, ok := parsedCert.(*ssh.Certificate)
if !ok {
common.Fatalln("failed to parse response certificate")
}

privateKeyBytes, err := ioutil.ReadFile(*sshPrivateKeyFilename)
common.FatalOnErrorW(err, "could not read the private SSH key")

privateKey, err := ssh.ParseRawPrivateKey(privateKeyBytes)
common.FatalOnErrorW(err, "could not parse the private SSH key")

sock, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK"))
common.FatalOnErrorW(err, "could not connect to the SSH agent socket, make sure SSH_AUTH_SOCK is set")
defer sock.Close()

sshAgent := agent.NewClient(sock)

pubcert := agent.AddedKey{
Comment: fmt.Sprintf("iam-request-ssh-key-signature env=\"%s\" expires=\"%s\"", *environment, response.ValidBefore),
PrivateKey: privateKey,
Certificate: cert,
LifetimeSecs: uint32(response.Duration),
}
err = sshAgent.Add(pubcert)
common.FatalOnErrorW(err, "could not add the certificate to the SSH agent")

case "stdout":
fmt.Println(string(ret.Payload))
default:
}

if *proxyConfig != "" {
conn, err := net.Dial("tcp", *proxyConfig)
if err != nil {
common.FatalOnErrorW(err, "could not connect to server")
}
ConnectIO(conn)
}

}
11 changes: 11 additions & 0 deletions lambda/sign-ssh-key/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,14 @@ a role with a policy allowing access to those resources.
[terraform.tf](./terraform.tf) gives an example of building and deploying the lambda using Secrets Manager to manage the CA
configuration. Apply terraform then set the fields in Secrets Manager. The private key should be base64 encoded to avoid issues
with new lines.

## Configuring the SSH servers

* Store the CA public key on your SSH server under `/etc/ssh/ca_user_key.pub`
* Edit `/etc/ssh/sshd_config` and append the following line `TrustedUserCAKeys /etc/ssh/ca_user_key.pub`
* Reload the SSH service `sudo /etc/init.d/ssh reload` or `sudo systemctl reload ssh`

## Creating the SSH users

The SSH certificate will use the IAM username as principal so the users need to exist on the SSH server.
The easiest way to do it is to use [iam-sync-users](../sync-users) on a CRON job.
2 changes: 1 addition & 1 deletion lambda/sign-ssh-key/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
var (
configFilenameTemplate = kingpin.Flag("config-filename-template", "Filename of the configuration file.").Default("%s.json").String()
eventFilename = kingpin.Flag("event-filename", "Filename with the event payload. Will process the event and exit if present.").String()
identityURLMaxAge = kingpin.Flag("identity-url-max-age", "Maximum age of the identity URL signature").Default("10s").Duration()
identityURLMaxAge = kingpin.Flag("identity-url-max-age", "Maximum age of the identity URL signature.").Default("10s").Duration()
)

func main() {
Expand Down
3 changes: 3 additions & 0 deletions lambda/sign-ssh-key/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ func (s Signer) Sign(key []byte, keyId string, principals, sourceAddresses []str
CertType: ssh.UserCert,
Permissions: ssh.Permissions{
CriticalOptions: criticalOptions,
Extensions: map[string]string{
"permit-pty": "",
},
},
}

Expand Down
3 changes: 2 additions & 1 deletion lambda/sign-ssh-key/terraform.tf
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ resource "aws_iam_role_policy_attachment" "read_ca_secret" {
resource "null_resource" "build" {

triggers = {
build = sha1(file("main.go"))
build = sha1(file("main.go"))
config = sha1(file("prod.json"))
}

provisioner "local-exec" {
Expand Down

0 comments on commit 12bfd5b

Please sign in to comment.