Skip to content

Commit

Permalink
Merge pull request #7 from micahhausler/self-hosted-guide
Browse files Browse the repository at this point in the history
Added self-hosted cluster setup guide
  • Loading branch information
jqmichael authored Sep 9, 2019
2 parents 2fd4bec + 2becc07 commit d2d6039
Show file tree
Hide file tree
Showing 3 changed files with 221 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ This will:
* Create the deployment, service, and mutating webhook in the cluster
* Approve the CSR that the deployment created for its TLS serving certificate
For self-hosted API server configuration, see see [SELF_HOSTED_SETUP.md](/SELF_HOSTED_SETUP.md)
### On API server
TODO
Expand Down
145 changes: 145 additions & 0 deletions SELF_HOSTED_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Self-hosted Kubernetes setup

If you are running your own Kubernetes cluster, there are several steps required for this feature to work.

This feature requires Kubernetes 1.12 or greater.

## Projected Token Signing Keypair

The first thing required is a new key pair for signing and verifying projected
service account tokens. This can be done using the following `ssh-keygen`
commands.

```bash
# Generate the keypair
PRIV_KEY="sa-signer.key"
PUB_KEY="sa-signer.key.pub"
PKCS_KEY="sa-signer-pkcs8.pub"
# Generate a key pair
ssh-keygen -t rsa -b 2048 -f $PRIV_KEY -m pem
# convert the SSH pubkey to PKCS8
ssh-keygen -e -m PKCS8 -f $PUB_KEY > $PKCS_KEY
```

## Public Issuer

As of 1.16, Kubernetes does not include an OIDC discovery endpoint itself (see
[kubernetes/community#1190](https://github.com/kubernetes/enhancements/pull/1190)),
so you will need to put your public signing key somewhere that AWS STS can
discover it. This example, we will create one in a public S3 bucket, but you
could host the following documents any way you'd like on a different domain.

### Create an S3 bucket

```bash
# Create S3 bucket with a random name. Feel free to set your own name here
export S3_BUCKET=${S3_BUCKET:-oidc-test-$(cat /dev/random | LC_ALL=C tr -dc "[:alpha:]" | tr '[:upper:]' '[:lower:]' | head -c 32)}
# Create the bucket if it doesn't exist
_bucket_name=$(aws s3api list-buckets --query "Buckets[?Name=='$S3_BUCKET'].Name | [0]" --out text)
if [ $_bucket_name == "None" ]; then
if [ "$AWS_REGION" == "us-east-1" ]; then
aws s3api create-bucket --bucket $S3_BUCKET
else
aws s3api create-bucket --bucket $S3_BUCKET --create-bucket-configuration LocationConstraint=$AWS_REGION
fi
fi
echo "export S3_BUCKET=$S3_BUCKET"
export HOSTNAME=s3-$AWS_REGION.amazonaws.com
export ISSUER_HOSTPATH=$HOSTNAME/$S3_BUCKET
```

### Create the OIDC discovery and keys documents

Part of the OIDC spec is to host an OIDC discovery and a keys JSON document.
Lets create these:

```bash
# Get the sha of the public key and use it as the key id
KID=$(sha1sum $PUB_KEY | awk '{print $1}')
cat <<EOF > discovery.json
{
"issuer": "https://$ISSUER_HOSTPATH/",
"jwks_uri": "https://$ISSUER_HOSTPATH/keys.json",
"authorization_endpoint": "urn:kubernetes:programmatic_authorization",
"response_types_supported": [
"id_token"
],
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"claims_supported": [
"sub",
"iss"
]
}
EOF
```

Included in this repo is a small go file to help create the keys json document.

```bash
go run ./hack/self-hosted/main.go -key $PKCS_KEY -kid $KID > keys.json
```

After you have the `keys.json` and `discovery.json` files, you'll need to place
them in your bucket. It is critical these objects are public so STS can access
them.

```bash
aws s3 cp --acl public-read ./discovery.json s3://$S3_BUCKET/.well-known/openid-configuration
aws s3 cp --acl public-read ./keys.json s3://$S3_BUCKET/keys.json
```

## Kubernetes API Server configuration

As of Kubernetes 1.12, Kubernetes can issue and mount projected service account
tokens in pods.

In order to use this feature, you'll need to set the following
[API server flags](https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/).

```
# This flag is likely already specified for legacy service accounts, you can
# specify this flag multiple times, and you'll need to add this with the path
# to the $PUB_KEY file from the beginning
--service-account-key-file
# Path to the signing (private) key ($PRIV_KEY)
--service-account-signing-key-file
# Identifiers of the API. The service account token authenticator will validate
# that tokens used against the API are bound to at least one of these audiences.
# If the --service-account-issuer flag is configured and this flag is not, this
# field defaults to a single element list containing the issuer URL.
#
# `--api-audiences` is for v1.13+, `--service-account-api-audiences` in v1.12
--api-audiences
# The issuer URL, or "https://$ISSUER_HOSTPATH" from above.
--service-account-issuer
```

## Audiences

The above `--api-audiences` flag sets an `aud` value for tokens that do not
request an audience, and the API server requires that any projected tokens used
for pod to API server authentication must have this audience set. This can
usually be set to `kubernetes.svc.default`, or optionally the DNS name of your
API server.

When using a Kubernetes-issued token for an external system, you should use a
different audience (or in OAuth-2 parlance, `client-id`). The external system
(such as AWS IAM) will usually require an audience, or client-id, at setup. For
AWS IAM, a token's `aud` must match the OIDC Identity Provider's client ID. EKS
uses the string `sts.amazonaws.com` as the default, but when using the webhook
yourself, you can use any audience you'd like as long as the webhook's flag
`--token-audience` is set to the same value as your IDP in IAM.

## Provider creation

From here, you can mostly follow the process in the [EKS
documentation](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html)
and substitue the cluster issuer with `https://$ISSUER_HOSTPATH`.
74 changes: 74 additions & 0 deletions hack/self-hosted/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package main

import (
"crypto/rsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"flag"
"fmt"
"io/ioutil"
"os"

"github.com/pkg/errors"
jose "gopkg.in/square/go-jose.v2"
)

type KeyResponse struct {
Keys []jose.JSONWebKey `json:"keys"`
}

func readKey(keyID, filename string) ([]byte, error) {
var response []byte
content, err := ioutil.ReadFile(filename)
if err != nil {
return response, errors.WithMessage(err, "error reading file")
}

block, _ := pem.Decode(content)
if block == nil {
return response, errors.Errorf("Error decoding PEM file %s", filename)
}

pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return response, errors.Wrapf(err, "Error parsing key content of %s", filename)
}
switch pubKey.(type) {
case *rsa.PublicKey:
default:
return response, errors.New("Public key was not RSA")
}

var alg jose.SignatureAlgorithm
switch pubKey.(type) {
case *rsa.PublicKey:
alg = jose.RS256
default:
return response, fmt.Errorf("invalid public key type %T, must be *rsa.PrivateKey", pubKey)
}

var keys []jose.JSONWebKey
keys = append(keys, jose.JSONWebKey{
Key: pubKey,
KeyID: keyID,
Algorithm: string(alg),
Use: "sig",
})

keyResponse := KeyResponse{Keys: keys}
return json.MarshalIndent(keyResponse, "", " ")
}

func main() {
kid := flag.String("kid", "", "The Key ID")
keyFile := flag.String("key", "", "The public key input file in PKCS8 format")
flag.Parse()

output, err := readKey(*kid, *keyFile)
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
fmt.Println(string(output))
}

0 comments on commit d2d6039

Please sign in to comment.