Skip to content

Instantly share code, notes, and snippets.

@dallasmarlow
Created March 12, 2022 18:17
Show Gist options
  • Save dallasmarlow/1a1589d30dc86fe0381e8a8752fce2f0 to your computer and use it in GitHub Desktop.
Save dallasmarlow/1a1589d30dc86fe0381e8a8752fce2f0 to your computer and use it in GitHub Desktop.
package main
import (
"crypto/sha1"
"crypto/tls"
"crypto/x509"
"errors"
"flag"
"fmt"
"log"
"net/url"
"os"
"path"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/eks"
"github.com/aws/aws-sdk-go/service/ssm"
)
var (
errCnt = 0
errInvalidScheme = errors.New("Invalid URL scheme, supported schemes: [https].")
certificateIndexFlag = flag.Int("cert-reverse-index", 0, " Reverse index of the certificate to fingerprint within chain, defaults to last cert defined.")
regionFlag = flag.String("region", "us-west-2", "AWS region.")
ssmKeyPrefixFlag = flag.String("ssm-key-prefix", "/eks_cluster_oidc_fingerprints/", "SSM parameter key prefix.")
ssmOverwriteFlag = flag.Bool("ssm-overwrite", false, "Overwrite SSM parameters.")
verifyCertChainFlag = flag.Bool("verify-cert-chain", true, "Verify TLS certificate chains on read.")
)
func clusters(svc *eks.EKS) ([]string, error) {
var clusters []string
log.Println("listing clusters")
err := svc.ListClustersPages(
&eks.ListClustersInput{},
func(resp *eks.ListClustersOutput, lastPage bool) bool {
for _, cluster := range resp.Clusters {
clusters = append(clusters, *cluster)
}
return !lastPage
},
)
return clusters, err
}
func clusterOidcIssuerURL(cluster string, svc *eks.EKS) (string, error) {
log.Println("describing cluster:", cluster)
response, err := svc.DescribeCluster(
&eks.DescribeClusterInput{
Name: aws.String(cluster),
},
)
if err != nil {
return "", err
}
return *response.Cluster.Identity.Oidc.Issuer, nil
}
func fingerprintTlsCert(cert *x509.Certificate) string {
return fmt.Sprintf("%x", sha1.Sum(cert.Raw))
}
func readTlsCerts(endpointURL string) ([]*x509.Certificate, error) {
parsedURL, err := url.Parse(endpointURL)
if err != nil {
return nil, err
}
if parsedURL.Scheme != "https" {
return nil, errInvalidScheme
}
if parsedURL.Port() == "" {
parsedURL.Host += ":443"
}
log.Println("reading certificates from endpoint:", endpointURL)
conn, err := tls.Dial(
"tcp",
parsedURL.Host,
&tls.Config{
InsecureSkipVerify: !*verifyCertChainFlag,
},
)
if err != nil {
return nil, err
}
defer conn.Close()
return conn.ConnectionState().PeerCertificates, nil
}
func run(eksService *eks.EKS, ssmService *ssm.SSM) {
eksClusters, err := clusters(eksService)
if err != nil {
log.Panicln("error - unable to list EKS clusters, err:", err)
}
for _, cluster := range eksClusters {
ssm_key := path.Join(*ssmKeyPrefixFlag, cluster)
if !*ssmOverwriteFlag {
log.Println("checking for existing SSM parameter at path:", ssm_key)
_, err := ssmService.GetParameter(&ssm.GetParameterInput{
Name: aws.String(ssm_key),
WithDecryption: aws.Bool(false),
})
if err == nil {
log.Println("SSM parameter already exists:", ssm_key)
continue
} else {
if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Code() != "ParameterNotFound" {
log.Println("error - unable to get SSM parameter:", ssm_key, "err:", err)
errCnt += 1
continue
}
}
}
}
isserUrl, err := clusterOidcIssuerURL(cluster, eksService)
if err != nil {
log.Println("error - unable to detect cluster OIDC issuer URL for cluster:", cluster)
errCnt += 1
continue
}
certs, err := readTlsCerts(isserUrl)
if err != nil {
log.Println("error - unable to read certificates from endpoint:", isserUrl, "err:", err)
errCnt += 1
continue
}
certIndex := len(certs) - *certificateIndexFlag - 1
certFingerprint := fingerprintTlsCert(certs[certIndex])
log.Println("setting SSM parameter:", ssm_key)
if _, err := ssmService.PutParameter(&ssm.PutParameterInput{
Name: aws.String(ssm_key),
Overwrite: aws.Bool(*ssmOverwriteFlag),
Type: aws.String("String"),
Value: aws.String(certFingerprint),
}); err != nil {
log.Println("error - unable to set SSM parameter:", ssm_key, "err:", err)
errCnt += 1
continue
}
}
}
func main() {
flag.Parse()
var region string
if envRegion := os.Getenv("AWS_REGION"); envRegion != "" {
log.Println("setting region from env variable:", envRegion)
region = envRegion
} else {
log.Println("setting region from flag input:", *regionFlag)
region = *regionFlag
}
awsSession := session.Must(
session.NewSession(
&aws.Config{
Region: aws.String(region),
},
),
)
run(
eks.New(awsSession),
ssm.New(awsSession),
)
if errCnt > 0 {
log.Fatalln("exiting with failures:", errCnt)
}
}
@dallasmarlow
Copy link
Author

dallasmarlow commented Mar 12, 2022

Typically when deploying EKS clusters using Terraform you can use the TLS provider to fetch the certificate fingerprint associated with the OIDC issuer associated with EKS clusters which is needed when provisioning OpenID connect providers. Example:

data "tls_certificate" "cluster_issuer" {
  url = aws_eks_cluster.eks_cluster.identity[0].oidc[0].issuer
}

resource "aws_iam_openid_connect_provider" "eks_cluster" {
  client_id_list = [
    "sts.amazonaws.com",
  ]
  thumbprint_list = [
    data.tls_certificate.cluster_issuer.certificates[0].sha1_fingerprint,
  ]
  url = aws_eks_cluster.eks_cluster.identity[0].oidc[0].issuer
}

The Terraform TLS provider uses golang's tls.Dial to make the connection to the OIDC issuer URLs which resolves the IP address and then makes an explicit connection to the IP directly. This can fail in execution envs where egress internet access is limited to domains using forward proxies or other access limitation scenarios prevent the connection from being established.

The code in eks_cert_fingerprint_indexer.go is an example for how to fetch the required TLS certificate fingerprint information outside of Terraform (this process can be used w/ a Lambda function for example) and store the values in SSM parameters which can be consumed by Terraform or other management systems.

The execution workflow of the utility is as follows:

  • list EKS clusters.
  • for each EKS cluster execute a describe cluster API call to fetch the OIDC issuer URL associated with each cluster.
  • for each OIDC issuer URL a TLS connection is established to fetch the certificates used by the endpoint.
  • a SSM parameter is created with the cluster name within the key and the fingerprint as the value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment