-
Notifications
You must be signed in to change notification settings - Fork 175
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #7 from micahhausler/self-hosted-guide
Added self-hosted cluster setup guide
- Loading branch information
Showing
3 changed files
with
221 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |