Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sdk/go): add kubernetes authentication provider #2703

Merged
merged 17 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e2b682e
feat(sdk/go): add kubernetes client token provider
GeorgeMac Jan 23, 2024
589302f
refactor(cmd/protoc-gen-go-flipt): rename KubernetesClientTokenProvid…
GeorgeMac Jan 23, 2024
27eb925
fix(cmd/protoc-gen-go-flipt-sdk): correct k8s constructor name and th…
GeorgeMac Jan 23, 2024
f2ff794
chore(sdk/go): update doc.go to account for KubernetesAuthenticationP…
GeorgeMac Jan 23, 2024
c8c98c3
test(integration): add cases for k8s authentication
GeorgeMac Jan 24, 2024
71e3d3f
fix(protoc-gen-go-flipt): correct typo in auth provider constructor name
GeorgeMac Jan 24, 2024
59d0228
chore(sdk/go): update docs for auth providers to link to associated m…
GeorgeMac Jan 24, 2024
d764fb5
Merge branch 'main' into gm/sdk-go-k8s
GeorgeMac Jan 24, 2024
5687d10
fix(test/integration): use corrected method name
GeorgeMac Jan 24, 2024
d3c7f25
Merge branch 'gm/sdk-go-k8s' of github.com:flipt-io/flipt into gm/sdk…
GeorgeMac Jan 24, 2024
9802b54
fix(github): move env var creation to before tests run
GeorgeMac Jan 24, 2024
9a0b079
fix(test/integration): ensure token service always enabled
GeorgeMac Jan 24, 2024
47faaaa
fix(test/integration): ensure token service always bootstrapped consi…
GeorgeMac Jan 24, 2024
f982935
Merge branch 'main' into gm/sdk-go-k8s
GeorgeMac Jan 24, 2024
4b38fdb
chore(dagger): upgrade from 0.9.4 to 0.9.5
GeorgeMac Jan 24, 2024
4cc3783
chore(test/integration): update documentation around serveOIDC
GeorgeMac Jan 24, 2024
ce529fe
refactor(testing/integration): move case construction into semaphore …
GeorgeMac Jan 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ jobs:
check-latest: true
cache: true

- run: echo "INTEGRATION_TEST_NAME=${{ matrix.test }}" | tr '/' '-' >> $GITHUB_ENV

- name: Install Dagger
run: |
cd /usr/local
Expand All @@ -83,8 +85,6 @@ jobs:
- name: Run Integration Tests
run: mage dagger:run "test:integration ${{ matrix.test }}"

- run: echo "INTEGRATION_TEST_NAME=${{ matrix.test }}" | tr '/' '-' >> $GITHUB_ENV

- name: Upload Flipt Service Logs
uses: actions/upload-artifact@v4
if: ${{ always() }}
Expand Down
80 changes: 80 additions & 0 deletions build/internal/cmd/discover/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package main

import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"flag"
"fmt"
"net/http"
"os"
"time"

"github.com/go-jose/go-jose/v3"
"github.com/hashicorp/cap/oidc"
)

const (
openidConfiguration = "/.well-known/openid-configuration"
wellKnownJwks = "/.well-known/jwks.json"
domain = "https://discover.svc"
)

func main() {
privKeyPath := flag.String("private-key", "", "path to private key")
addr := flag.String("addr", ":443", "Port for OIDC server")
flag.Parse()

p, err := os.ReadFile(*privKeyPath)
if err != nil {
panic(err)
}

block, _ := pem.Decode([]byte(p))
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
panic(err)
}

handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}

switch r.URL.Path {
case openidConfiguration:
reply := struct {
Issuer string `json:"issuer"`
JWKSURI string `json:"jwks_uri"`
SupportedAlgs []string `json:"id_token_signing_alg_values_supported"`
}{
Issuer: domain,
JWKSURI: domain + wellKnownJwks,
SupportedAlgs: []string{string(oidc.RS256)},
}

if err := json.NewEncoder(w).Encode(&reply); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
case wellKnownJwks:
if err := json.NewEncoder(w).Encode(&jose.JSONWebKeySet{
Keys: []jose.JSONWebKey{
{
Key: &priv.PublicKey,
KeyID: fmt.Sprintf("%d", time.Now().Unix()),
},
},
}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
default:
http.Error(w, "not found", http.StatusNotFound)
return
}
})

_ = http.ListenAndServeTLS(*addr, "/server.crt", "/server.key", handler)
}
181 changes: 164 additions & 17 deletions build/testing/integration.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package testing

import (
"bytes"
"context"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"log"
"math/big"
"os"
"path"
"strings"
Expand Down Expand Up @@ -99,6 +102,7 @@ const (
staticAuth
staticAuthNamespaced
jwtAuth
k8sAuth
)

func (a authConfig) enabled() bool {
Expand All @@ -111,6 +115,8 @@ func (a authConfig) method() string {
return "static"
case jwtAuth:
return "jwt"
case k8sAuth:
return "k8s"
default:
return ""
}
Expand All @@ -135,7 +141,7 @@ func Integration(ctx context.Context, client *dagger.Client, base, flipt *dagger

for _, namespace := range []string{"", "production"} {
for protocol, port := range protocolPorts {
for _, auth := range []authConfig{noAuth, staticAuth, staticAuthNamespaced, jwtAuth} {
for _, auth := range []authConfig{noAuth, staticAuth, staticAuthNamespaced, jwtAuth, k8sAuth} {
auth := auth
config := testConfig{
name: fmt.Sprintf("%s namespace %s", strings.ToUpper(protocol), namespace),
Expand All @@ -154,6 +160,8 @@ func Integration(ctx context.Context, client *dagger.Client, base, flipt *dagger
config.name = fmt.Sprintf("%s with static auth namespaced token", config.name)
case jwtAuth:
config.name = fmt.Sprintf("%s with jwt auth", config.name)
case k8sAuth:
config.name = fmt.Sprintf("%s with k8s auth", config.name)
}

configs = append(configs, config)
Expand All @@ -168,28 +176,50 @@ func Integration(ctx context.Context, client *dagger.Client, base, flipt *dagger
var (
fn = fn
config = config
flipt = flipt
base = base
)

flipt := flipt
if config.auth.enabled() {
bytes, err := x509.MarshalPKIXPublicKey(priv.Public())
if err != nil {
return err
}

bytes = pem.EncodeToMemory(&pem.Block{
Type: "public key",
Bytes: bytes,
})

flipt = flipt.
WithEnvVariable("FLIPT_AUTHENTICATION_REQUIRED", "true").
WithEnvVariable("FLIPT_AUTHENTICATION_METHODS_TOKEN_ENABLED", "true").
WithEnvVariable("FLIPT_AUTHENTICATION_METHODS_TOKEN_BOOTSTRAP_TOKEN", bootstrapToken).
WithEnvVariable("FLIPT_AUTHENTICATION_METHODS_JWT_ENABLED", "true").
WithNewFile("/etc/flipt/jwt.pem", dagger.ContainerWithNewFileOpts{Contents: string(bytes)}).
WithEnvVariable("FLIPT_AUTHENTICATION_METHODS_JWT_PUBLIC_KEY_FILE", "/etc/flipt/jwt.pem").
WithEnvVariable("FLIPT_AUTHENTICATION_METHODS_JWT_VALIDATE_CLAIMS_ISSUER", "https://flipt.io")
WithEnvVariable("FLIPT_AUTHENTICATION_METHODS_TOKEN_BOOTSTRAP_TOKEN", bootstrapToken)

switch config.auth {
case k8sAuth:
flipt = flipt.
WithEnvVariable("FLIPT_AUTHENTICATION_METHODS_KUBERNETES_ENABLED", "true")

var saToken string
// run an OIDC server which exposes a JWKS url using a private key we own
// and generate a JWT to act as our SA token
flipt, saToken, err = serveOIDC(ctx, client, base, flipt)
if err != nil {
return err
}

// mount service account token into base on expected k8s sa token path
base = base.WithNewFile("/var/run/secrets/kubernetes.io/serviceaccount/token", dagger.ContainerWithNewFileOpts{
Contents: saToken,
})
case jwtAuth:
bytes, err := x509.MarshalPKIXPublicKey(priv.Public())
if err != nil {
return err
}

bytes = pem.EncodeToMemory(&pem.Block{
Type: "public key",
Bytes: bytes,
})

flipt = flipt.
WithEnvVariable("FLIPT_AUTHENTICATION_METHODS_JWT_ENABLED", "true").
WithNewFile("/etc/flipt/jwt.pem", dagger.ContainerWithNewFileOpts{Contents: string(bytes)}).
WithEnvVariable("FLIPT_AUTHENTICATION_METHODS_JWT_PUBLIC_KEY_FILE", "/etc/flipt/jwt.pem").
WithEnvVariable("FLIPT_AUTHENTICATION_METHODS_JWT_VALIDATE_CLAIMS_ISSUER", "https://flipt.io")
}
}

name := strings.ToLower(replacer.Replace(fmt.Sprintf("flipt-test-%s-config-%s", caseName, config.name)))
Expand Down Expand Up @@ -396,6 +426,7 @@ func git(ctx context.Context, client *dagger.Client, base, flipt *dagger.Contain
func s3(ctx context.Context, client *dagger.Client, base, flipt *dagger.Container, conf testConfig) func() error {
minio := client.Container().
From("quay.io/minio/minio:latest").
WithEnvVariable("UNIQUE", uuid.New().String()).
WithExposedPort(9009).
WithEnvVariable("MINIO_ROOT_USER", "user").
WithEnvVariable("MINIO_ROOT_PASSWORD", "password").
Expand Down Expand Up @@ -625,6 +656,7 @@ func suite(ctx context.Context, dir string, base, flipt *dagger.Container, conf
func azblob(ctx context.Context, client *dagger.Client, base, flipt *dagger.Container, conf testConfig) func() error {
azurite := client.Container().
From("mcr.microsoft.com/azure-storage/azurite").
WithEnvVariable("UNIQUE", uuid.New().String()).
WithExposedPort(10000).
WithExec([]string{"azurite-blob", "--blobHost", "0.0.0.0", "--silent"}).
AsService()
Expand Down Expand Up @@ -657,6 +689,7 @@ func azblob(ctx context.Context, client *dagger.Client, base, flipt *dagger.Cont
func gcs(ctx context.Context, client *dagger.Client, base, flipt *dagger.Container, conf testConfig) func() error {
gcs := client.Container().
From("fsouza/fake-gcs-server").
WithEnvVariable("UNIQUE", uuid.New().String()).
WithExposedPort(4443).
WithExec([]string{"-scheme", "http", "-public-host", "gcs:4443"}).
AsService()
Expand Down Expand Up @@ -697,3 +730,117 @@ func signJWT(key crypto.PrivateKey, claims interface{}) string {

return raw
}

func serveOIDC(ctx context.Context, client *dagger.Client, base, flipt *dagger.Container) (*dagger.Container, string, error) {
priv, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return nil, "", err
}

rsaSigningKey := &bytes.Buffer{}
if err := pem.Encode(rsaSigningKey, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(priv),
}); err != nil {
return nil, "", err
}

fliptSAToken := signJWT(priv, map[string]any{
"exp": time.Now().Add(24 * time.Hour).Unix(),
"iss": "https://discover.srv",
"kubernetes.io": map[string]any{
"namespace": "flipt",
"pod": map[string]any{
"name": "flipt-7d26f049-kdurb",
"uid": "bd8299f9-c50f-4b76-af33-9d8e3ef2b850",
},
"serviceaccount": map[string]any{
"name": "flipt",
"uid": "4f18914e-f276-44b2-aebd-27db1d8f8def",
},
},
})

ca := &x509.Certificate{
SerialNumber: big.NewInt(2019),
Subject: pkix.Name{
Organization: []string{"Flipt, INC."},
Country: []string{"US"},
Province: []string{""},
Locality: []string{"North Carolina"},
StreetAddress: []string{""},
PostalCode: []string{""},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0),
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
DNSNames: []string{"discover.svc"},
}

caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return nil, "", err
}

caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
if err != nil {
return nil, "", err
}

var caCert bytes.Buffer
if err := pem.Encode(&caCert, &pem.Block{
Type: "CERTIFICATE",
Bytes: caBytes,
}); err != nil {
return nil, "", err
}

var caPrivKeyPEM bytes.Buffer
pem.Encode(&caPrivKeyPEM, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey),
})

return flipt.
WithEnvVariable("FLIPT_LOG_LEVEL", "debug").
WithEnvVariable("FLIPT_AUTHENTICATION_METHODS_KUBERNETES_DISCOVERY_URL", "https://discover.svc").
WithServiceBinding("discover.svc", base.
WithNewFile("/server.crt", dagger.ContainerWithNewFileOpts{
Contents: caCert.String(),
}).
WithNewFile("/server.key", dagger.ContainerWithNewFileOpts{
Contents: caPrivKeyPEM.String(),
}).
WithNewFile("/priv.pem", dagger.ContainerWithNewFileOpts{
Contents: rsaSigningKey.String(),
}).
WithExposedPort(443).
WithExec([]string{
"sh",
"-c",
"go run ./build/internal/cmd/discover/... --private-key /priv.pem",
}).
AsService()).
WithNewFile("/var/run/secrets/kubernetes.io/serviceaccount/token",
dagger.ContainerWithNewFileOpts{Contents: fliptSAToken}).
WithNewFile("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt",
dagger.ContainerWithNewFileOpts{Contents: caCert.String()}),
signJWT(priv, map[string]any{
"exp": time.Now().Add(24 * time.Hour).Unix(),
"iss": "https://discover.svc",
"kubernetes.io": map[string]any{
"namespace": "integration",
"pod": map[string]any{
"name": "integration-test-7d26f049-kdurb",
"uid": "bd8299f9-c50f-4b76-af33-9d8e3ef2b850",
},
"serviceaccount": map[string]any{
"name": "integration-test",
"uid": "4f18914e-f276-44b2-aebd-27db1d8f8def",
},
},
}), nil
}
GeorgeMac marked this conversation as resolved.
Show resolved Hide resolved
8 changes: 4 additions & 4 deletions build/testing/integration/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1392,12 +1392,12 @@ func API(t *testing.T, ctx context.Context, client sdk.SDK, opts integration.Tes
t.Run("Auth", func(t *testing.T) {
t.Run("Self", func(t *testing.T) {
_, err := client.Auth().AuthenticationService().GetAuthenticationSelf(ctx)
if authConfig == integration.StaticTokenAuth || authConfig == integration.JWTAuth {
// only valid with a non-scoped token
assert.NoError(t, err)
} else {
if !authConfig.Required() || authConfig.NamespaceScoped() {
assert.EqualError(t, err, "rpc error: code = Unauthenticated desc = request was not authenticated")
return
}

assert.NoError(t, err)
})
t.Run("Public", func(t *testing.T) {
_, err := client.Auth().PublicAuthenticationService().ListAuthenticationMethods(ctx)
Expand Down
Loading
Loading