Octelium documentation · Latest

Clientless Access for Workloads to Kubernetes

You can easily protect access to all your Kubernetes clusters as Octelium Services and provide your clientless WORKLOAD Users such as your Golang-based microservices and applications with secretless access without having to expose, manage and share Kubeconfigs, mTLS client private keys or access tokens required to access such Kubernetes clusters. In this short guide, we're going to use the Golang SDK (read more here) together with the official Kubernetes Golang client to access the resources of a protected Kubernetes cluster.

We first create a Secret that contains the kubeconfig file required to access the Kubernetes cluster (read more here) as follows:

octeliumctl create secret kubeconfig-k8s1 --file /PATH/TO/KUBECONFIG

Note that Octelium also supports secretless access to Kubernetes clusters via access tokens and mTLS client certificates. You can read more here.

Now we create the KUBERNETES Service representing our Kubernetes cluster that needs to be protected. The Service needs to be publicly exposed in order to be accessible via the clientless mode (read more here) as follows:

kind: Service metadata: name: k8s1 spec: mode: KUBERNETES isPublic: true config: upstream: url: https://k8s-cluster.example.com:6443 kubernetes: kubeconfig: fromSecret: kubeconfig-k8s1

You can now apply the Service k8s1 as follows:

octeliumctl apply /PATH/TO/SERVICE.YAML

Now you have multiple options to access this Service from your applications, namely via the Octelium Golang SDK, via OAuth2 client credentials or directly via bearer access tokens. The last 2 options allow you to use standard OAuth2 client credentials and bearer tokens without having to use any special SDKs or being aware of the Octelium Cluster existence at all.

Golang SDK

We first create an authentication token Credential for the WORKLOAD User representing our application, which we name here microservice1, whose Sessions are CLIENTLESS (read more about authentication token Credentials here and Session types here) as follows:

octeliumctl create cred --user microservice1 --session-type clientless microservice1-cred # The output should be something like Authentication Token: AQpA0f-8AQE2SLLZG3UDvvi...

Now we add the octelium-go library as well as the Kubernetes Golang client to our application as follows:

go get github.com/octelium/octelium/octelium-go go get k8s.io/client-go@latest

The HTTPClient() method of the octelium-go client returns a standard *http.Client that attaches the Session access token to every request and automatically refreshes it before it expires. We can simply feed it to the NewForConfigAndClient() function in order to create a Kubernetes client as follows:

package main import ( "context" "fmt" "os" octelium "github.com/octelium/octelium/octelium-go" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" ) func main() { if err := run(context.Background()); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func run(ctx context.Context) error { client, err := octelium.New(ctx, octelium.WithDomain("example.com"), octelium.WithAuthenticator( octelium.AuthenticationToken(os.Getenv("OCTELIUM_AUTH_TOKEN")), ), ) if err != nil { return err } defer client.Close() k8sClient, err := kubernetes.NewForConfigAndClient(&rest.Config{ Host: "https://k8s1.example.com", }, client.HTTPClient()) if err != nil { return err } pods, err := k8sClient.CoreV1().Pods("").List(ctx, metav1.ListOptions{}) if err != nil { return err } for _, pod := range pods.Items { fmt.Printf("Pod %s/%s is %s\n", pod.Namespace, pod.Name, pod.Status.Phase) } return nil }

Note that the host must be the Service's public HTTPS URL, since the access token is only attached to HTTPS requests to the Cluster domain and its subdomains by default. Since the Service provides secretless access to the upstream Kubernetes cluster, your application does not need any kubeconfig, client certificates or Kubernetes tokens at all.

note

You can also omit the options and let octelium.New() read the Cluster domain and the authentication token from the OCTELIUM_DOMAIN and OCTELIUM_AUTH_TOKEN environment variables respectively.

Secretless Authentication via OIDC Assertions

If your application itself runs inside a Kubernetes cluster, then it does not even need an authentication token Credential. Instead, it can authenticate itself to the Cluster using a Kubernetes-issued OIDC identity token of its own Kubernetes service account (read more about assertion-based IdentityProviders here). This way no secret at all is stored inside the Kubernetes cluster hosting your application.

First we create an oidcIdentityToken IdentityProvider for the Kubernetes cluster hosting your application as follows:

kind: IdentityProvider metadata: name: apps-cluster spec: oidcIdentityToken: issuerURL: https://oidc.apps-cluster.example.com audience: https://example.com
note

Many managed Kubernetes providers publish their OIDC discovery endpoints publicly. If your Kubernetes cluster does not, then you can set the JWKS content of the cluster manually instead of the issuerURL field (read more here).

Now your WORKLOAD User can set the identity that corresponds to the Kubernetes service account of your application as follows:

kind: User metadata: name: microservice1 spec: type: WORKLOAD authentication: identities: - identityProvider: apps-cluster identifier: system:serviceaccount:default:microservice1

The identifier of the User's identity is the sub claim of the Kubernetes service account token which takes the form system:serviceaccount:<K8S_NAMESPACE>:<K8S_SERVICE_ACCOUNT_NAME>. Now we mount a short-lived, audience-bound projected service account token in the container of your application. Note that the token audience has to match the audience field of the IdentityProvider:

apiVersion: apps/v1 kind: Deployment metadata: name: microservice1 namespace: default spec: selector: matchLabels: app: microservice1 template: metadata: labels: app: microservice1 spec: serviceAccountName: microservice1 containers: - name: microservice1 image: <YOUR_IMAGE> env: - name: OCTELIUM_DOMAIN value: example.com - name: OCTELIUM_ASSERTION_FILE value: /var/run/secrets/octelium/token volumeMounts: - name: octelium-token mountPath: /var/run/secrets/octelium readOnly: true volumes: - name: octelium-token projected: sources: - serviceAccountToken: path: token audience: https://example.com expirationSeconds: 3600

Kubernetes itself rotates the projected token before it expires. Since OCTELIUM_DOMAIN and OCTELIUM_ASSERTION_FILE are set, the application can now simply create its octelium-go client without any options:

client, err := octelium.New(ctx)

Which is equivalent to explicitly setting the authenticator as follows:

client, err := octelium.New(ctx, octelium.WithDomain("example.com"), octelium.WithAuthenticator( octelium.AssertionFromFile("/var/run/secrets/octelium/token"), ), )

The assertion file is read upon every authentication, so the library can always create a new Session with the freshly rotated token whenever the previous Session expires.

OAuth2 Client Credentials

If you do not want to use the Octelium SDK, you can instead use the standard OAuth2 client credentials flow (read more here). You can create an OAuth2 client credentials Credential as follows:

octeliumctl create cred --type oauth2 --user microservice1 cred02 Client ID: spxg-cdyx Client Secret: AQpAN9OT1az6DQH69dNklhr...

Now you can use the clientcredentials package to obtain the access token and automatically obtain a new one once it expires, and wrap the Kubernetes client transport in order to attach the access token to every request as follows:

package main import ( "context" "fmt" "net/http" "os" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" ) func main() { if err := run(context.Background()); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func run(ctx context.Context) error { domain := "example.com" config := clientcredentials.Config{ ClientID: os.Getenv("OCTELIUM_CLIENT_ID"), ClientSecret: os.Getenv("OCTELIUM_CLIENT_SECRET"), TokenURL: fmt.Sprintf("https://%s/oauth2/token", domain), AuthStyle: oauth2.AuthStyleInParams, } // The token source caches the access token and obtains a new one before // it expires. tokenSource := config.TokenSource(ctx) k8sClient, err := kubernetes.NewForConfig(&rest.Config{ Host: fmt.Sprintf("https://k8s1.%s", domain), WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return &oauth2.Transport{Source: tokenSource, Base: rt} }, }) if err != nil { return err } pods, err := k8sClient.CoreV1().Pods("").List(ctx, metav1.ListOptions{}) if err != nil { return err } for _, pod := range pods.Items { fmt.Printf("Pod %s/%s is %s\n", pod.Namespace, pod.Name, pod.Status.Phase) } return nil }

If you want to use access token Credentials (read more here) instead of OAuth2 client credentials Credentials, you can simply set the access token in the BearerToken field of the rest.Config instead.

Octelium also provides OpenTelemetry-ready, application-layer L7 aware visibility and access logging in real time (see an example for Kubernetes here). You can read more about visibility here.