Kubernetes: How to write a kubectl plugin

Hacktoberfest is almost over but since there’re plenty of opportunities to contribute, I decided to takeover the task of re-writing a kubectl plugin called view-secret. For those of you who are not familar with kubectl, it’s the CLI tool to work with Kubernetes. In this post I’d like to shed some light on krew and what’s necessary to create your very own plugin. Okay, so what’s krew?

Krew is one of the many Kubernetes Special Interest Groups (SIG) and aims at solving the package management issue for kubectl. There’s a limited amount of core functionality that ships with kubectl so krew is all about allowing developers to create their own extensions and contribute them back to the community. All available plugins are stored in the krew-index, a central repo that plugin maintainers use to publish/update their plugins. If you haven’t used krew before, make sure to install it first.

Some early plugins have been written in Bash and the maintainer was asking the community to take over the re-write in Go/Python and ultimately the maintenance of the plugins. Since my day job requires me to create & manage Kubernetes-based deployments, I find myself in need of decoding secrets all the time. A secret - as the name may reveal already - is used to store secret information Base64 encoded. This resource type is often used to populate environment configuration for deployments, to store docker registry auth information or tls secrets.

The typical workflow to decode a secret without view-secret looks as follows:

  1. kubectl get secret <secret> -o yaml
  2. Copy base64 encoded secret
  3. echo "b64string" | base64 -d

This gets quite cumbersome especially if you just want to check the entirety of a secret to see if everything looks ok. There are solutions like kubedecode or the previous view-secret implementation that aim at solving this problem but lack either native kubectl integration, are outdated/not maintained anymore or require you to always provide e.g. the namespace as a parameter.

So I went ahead and created a new implementation for view-secret that is backward-compatible to the existing implementation but also adds a new capability, namely decoding all contents of a secret. My contribution has been accepted and the plugin is available now, so let me walk you through the process.

As it turns out, creating your own plugin is super simple and well documented here. All you have to do is create a binary with the prefix kubectl-, make it executable and place it somewhere in your $PATH. A sample plugin can be as easy as this:

# kubectl-hello plugin
cat <<EOF > kubectl-hello
#!/usr/bin/env bash
echo "hello from krew"
EOF

# Make executable
chmod +x kubectl-hello

# Copy into some $PATH location
cp kubectl-hello /usr/local/bin

# Run plugin
kubectl hello
## prints "hello from krew"

Since my language of choice for this project was Go, I created a new project and added integrations such as GoReleaser to simplify shipping the binary for mulitple platforms and Travis CI to automate running builds/creating releases. To simplify the build/test process I also added a Makefile. At this point my project repo had the following layout:

cmd
  kubectl-view-secret.go
pkg
  cmd
    view-secret.go
    view-secret_test.go
go.mod
go.sum
.goreleaser.yml
.travis.yml
Makefile

The established workflow was pretty straight-forward:

  1. push changes to master –> triggers travis to run tests
  2. tag commit –> triggers travis to use goreleaser

I previously wrote about the usage of Makefiles in Go projects but for this project the targets are much simpler:

SOURCES := $(shell find . -name '*.go')
BINARY := kubectl-view-secret

build: kubectl-view-secret

test: $(SOURCES)
  go test -v -short -race -timeout 30s ./...

$(BINARY): $(SOURCES)
  CGO_ENABLED=0 go build -o $(BINARY) -ldflags="-s -w" ./cmd/$(BINARY).go

For the actual implementation I used spf13/cobra to parse flags and process user input. To get the secret contents I use exec.Command thus shelling out to the OS instead of using the kubernetes go client or cli runtime as they add a huge overhead for such a small functionality.

After I finished the implementation, all I had to do was update the plugins/view-secret.yaml spec in the krew index to use my new plugin. This meant changing the plugin description, the download links for the new binaries and the sha256 checksums. Once the Pull Request got merged, the local plugin index had to be updated via kubectl krew update and the plugin can be installed via kubectl krew install view-secret.

Now the workflow to decode secrets is as simple as this:

# print secret keys
kubectl view-secret <secret>

# decode specific entry
kubectl view-secret <secret> <key>

# decode all secret contents
kubectl view-secret <secret> -a/--all

# print keys for secret in different namespace
kubectl view-secret <secret> -n/--namespace foo

# suppress info output
kubectl view-secret <secret> -q/--quiet

This was my first CNCF contribution & I’m happy about the feedback I got from @ahmetb & @corneliusweig throughout the process.

The full plugin code is available on GitHub.

Thanks for reading! As always please reach out if you have any questions. :wave:

Tags: krew, kubectl, plugin, kubernetes, k8s, cncf, hacktoberfest

Kubernetes: oauth2_proxy with dynamic callback urls

We all love the simplicity of deploying applications on Kubernetes and while many tutorials out there help you get started quickly and provide a great resource for many, some of them spare important details. In this post, I try to help the community by providing a small guide on how to deploy oauth2_proxy with dynamic callback urls. But first, what is oauth2_proxy and which problem does it solve?

The README.md explains it as follows:

A reverse proxy and static file server that provides authentication using Providers (Google, GitHub, and others) to validate accounts by email, domain or group.

Okay great, so this tool comes in handy if you want to authenticate users for an application that doesn’t offer authentication by itself. Famous examples include the Kubernetes dashboard, Prometheus or Alertmanager.

There are multiple ways to solve the issue of serving apps that don’t offer authentication out-of-the-box:

  1. Don’t expose it at all and just browse it using kubectl port-forward. This way, the application is never publicly exposed on the internet.
  2. Expose it and handle authentication in a proxy sitting in front of the application using oauth2_proxy via existing providers (Microsoft, GitHub, etc.).
  3. Establish your own (federated) idendity provider to handle user authentication using i.e. dex.

For the first option everyone who needs access to these tools need cluster access, so this is not a very flexible option. The second option is definitely more interesting because we can safely expose the applications on the public internet without much effort. The third option offer the most flexiblity but is a bit of an overkill for what I’m trying to achieve. Hence, I’ll be focusing on No. 2. But we might not want to have mulitple proxies in place which handle authentication independently for the respective app but rather a single instance that can be used by all apps. Let’s go through an example:

I want to expose Alertmananger and Prometheus to the same group of people and they should be able to seemlessly switch between the applications without the need to sign-in again.

First, I’ll be using helm to install the chart for oauth2_proxy and setting some custom properties which I need for the OAuth2 provider of my choice:

helm install stable/oauth2-proxy --name login-oauth2-proxy \
--namespace xyz \
--set config.clientID="${CLIENT_ID}" \
--set config.clientSecret="${CLIENT_SECRET}" \
--set config.cookieSecret="${COOKIE_SECRET}" \
--set extraArgs.provider="azure" \
--set extraArgs.azure-tenant="${AZURE_TENANT_ID}" \
--set extraArgs.whitelist-domain=".mydomain.com" \
--set extraArgs.cookie-domain=".mydomain.com" \
--tls

clientID, clientSecret and azure-tenant can be obtained upon registering the application for Azure Active Directory integration as described here. To restrict access to only a subset of users from the Active Directory make sure to follow these instructions after. It’s important to register the url of the ingress rule that will be used for authentication (see below), in my case https://login.mydomain.com/oauth2/callback.

The cookieSecret is just a random secret that can be generated with a simple python script.

docker run -ti --rm python:3-alpine python -c 'import secrets,base64; print(base64.b64encode(base64.b64encode(secrets.token_bytes(16))));'

Worth mentioning are the whilelist-domain and cookie-domain flags which should point to the parent domain of the applications to be protected, i.e.
You want to protect prom.mydomain.com and alerts.mydomain.com then this needs to be set to .mydomain.com.

Great, now we need an ingress route that handles authentication via the proxy we just deployed. This looks as simple as this:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: login-ingress-oauth2
  namespace: xyz
  annotations:
    kubernetes.io/ingress.class: nginx
spec:
  rules:
  - host: login.mydomain.com
    http:
      paths:
      - backend:
          serviceName: login-oauth2-proxy
          servicePort: 80
        path: /oauth2
  tls:
  - hosts:
    - login.mydomain.com

Now all we have to do for the application that should be protected via our proxy is to use the auth-url and auth-signin annotations of nginx and have them reference this ingress route.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: alertmanager-ingress
  namespace: tiller
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/auth-url: "https://login.mydomain.com/oauth2/auth"
    nginx.ingress.kubernetes.io/auth-signin: "https://login.mydomain.com/oauth2/start?rd=https://$host$request_uri"
spec:
  rules:
  - host: alerts.mydomain.com
    http:
      paths:
      - backend:
          serviceName: prom-prometheus-operator-alertmanager
          servicePort: 9093
        path: /
  tls:
  - hosts:
    - alerts.mydomain.com

Browsing alerts.mydomain.com will redirect to the microsoft login and after successful authentication back to the application. If you deploy multiple application using this method you won’t have to login again as the consent has been granted already and a valid cookie exists.

A few things to be mentioned:

  1. Depending on how many applications rely on the proxy, you might want to scale the oauth2_proxy deployment to ensure availability
  2. None of the explanations above indicate that you shouldn’t be taking care of proper RBAC rules in your cluster and restrict access to the applications according to the principle of least privilege.

That’s it for now, please reach out if you have further questions or remarks. Happy Sunday!

Tags: oauth2_proxy, nginx, sso, single sign-on, reverse proxy, nginx, kubernetes

Containerization of Golang applications

I’ve been a working a lot in Golang recently and even though it easily allows for single static binary compilation I find myself using Docker a lot. Why? Well, especially when it comes to container orchestration and scaling of workloads some sort of container technology is used as these build the foundation of a Pod in Kubernetes (our deployment infrastructure).

While coming up with an initial Dockerfile is easy, correctly compiling a Go application and adhere to all sorts of container best practices is still hard. Does the size of the binary matter? If so, you might want to provide some additional flags during compilation. Do you build the app outside of the container and just copy the final artifact into it or do you use multi-stage builds to have everything within an isolated environment? How do you deal with caching of layers? How do you make sure that the final container is secure and only contains whatever is needed to execute your application?

The first thing I was overwhelmed with is how much old information is still out there and that even well-known developers such as Tim Hockin (co-creator of Kubernetes) are having questions on how to actually compile a Golang application correctly. As it turns out, some flags are strictly unnecessary as of Go 1.10 but are still widely used. Ultimately, it all depends on your needs and whether you need cgo or not but even after studying a lot of blog posts I’m still not 100% sure about my approach. As it turns out, Tim created a nice skeleton project which is a good starting point in my opinion.

Furthermore, I saw a lot of different approaches in terms of runtime base image and how the build process takes place. Some are using for golang:alpine with manually installing ca-certs, tzinfo, etc. during the build stage whereas others use plain golang instead. For the final stage common choices are either scratch or alpine which still provide a larger attack surface than i.e. gcr.io/distroless/base. As with many things, there’s not a single correct approach because one might want to keep the ability to docker exec -it into a container around whereas others have better ways to debug their services.

While coming up with my current solution I had the following considerations to take into account. Local development should still be fast, the build process must be CI-friendly with clean & reproduceable builds and no additional tooling needed to secure the final image such as microscanner or clair. Hence, I created a Makefile that helps me take care of the heavy lifting and allows for fast local development where no Docker is used at all. A shortened & simplified version looks as follows:

OUT := binary-name
PKG := github.com/package
VERSION := $(shell git describe --always --dirty)
PKG_LIST := $(shell go list ${PKG}/...)
GO_FILES := $(shell find . -name '*.go')

build:
	go build -i -v -o ${OUT} -ldflags="-X main.version=${VERSION}" ${PKG}

test:
	@go test -short ${PKG_LIST}

vet:
	@go vet ${PKG_LIST}

errorcheck:
	@errcheck ${PKG_LIST}

lint:
	@for file in ${GO_FILES} ;  do \
		golint $$file ; \
	done

container:
	@docker build --build-arg VERSION=${VERSION} -t registry/image:${VERSION} .

container-push:
	@docker push registry/image:${VERSION}

run: build
	./${OUT}

clean:
	-@rm ${OUT} ${OUT}-*

.PHONY: run build vet lint errorcheck

I’ll talk about -ldflags in a bit, so don’t worry about it for now. Since the regular go build command doesn’t do static analysis on the project files, I created steps like vet (checks for correctness/suspicious constructs), lint (style mistakes) and errorcheck (missing error handling) I can run whenever I feel like it. This is not done implicitly through another step such as build because my CI system takes care of these things too. The rest of the file should be self-explanatory if you’re familiar with make.
Now, the following Dockerfile is only used in my CI system for which I don’t mind it to fetch the dependencies during each build.

# Build stage
FROM golang:1.11.4 AS build-env

LABEL maintainer="Jonas-Taha El Sesiy <[email protected]>"

WORKDIR /project
ARG VERSION
COPY main.go go.mod go.sum ./
RUN bash -c "go get -d &> /dev/null" && \
    CGO_ENABLED=0 GOOS=linux go build -ldflags "-X main.version=${VERSION} -s -w" -a -o app .

# Final stage
FROM gcr.io/distroless/base
COPY --from=build-env /project/app .
CMD ["./app"]

I’m using multi-stage builds with the latest Golang version as the base image. For the final stage, I opted for distroless even though the final image is bigger than the other choices. Note that I’m using go modules for dependency management introduced in Go 1.11 for which I copy the go.mod and go.sum files into the container.
As mentioned before, there are a couple of flags passed onto the go compiler via -ldflags. -X main.version=abc allows me to pass on the version information to the binary which is then used within the app in some fashion. -s -w disables the symbol table and the generation of debug data in form of DWARF in order to reduce the size of the binary which is useful for my production image.

This is just my take on this. If you have suggestions for improvements or any other remarks, please reach out. Thanks! :wave:

Tags: docker, container, golang, go

DV lottery - What is that?, Part 3

Alright, this is the last part of the series on the ins and outs of the DV lottery. If you haven’t read the previous articles already, make sure to find out everything about the interview process in the first part and the some of the initial actions to take arriving in the country if you’re among the lucky ones in the second part.
In this part, I’ll be talking about getting a driver’s license, job search, and international travel as a resident (how to keep your status as a resident).

Driver’s license

As a newcomer to a foreign country you’re required to operate a vehicle at some point in time. If I think about my time as a tourist in the US it was easy as many states allow driving with a foreign driver’s license up to three months. Well, if you’re on an immigrant visa it’s different. In California where I live, the law says you’re only allowed to use a foreign license for 10 days if there’s an immigration intent. In crowded places like the San Fransisco Bay Area it’s almost impossible to get an appointment that fast.
If you’re hoping for a clear guidance on what to do in this case - I don’t have it.

Some people keep driving with their foreign license, some people use any of the available car sharing services to get around. The good thing is though, that once you passed the knowledge test you’re allowed to drive if someone at the age of 25 who owns a valid driver’s license accompanies you. This should help you to familarize yourself with your new environment and lets you practice for your driving test. For me the whole process took about 3 month and cost 35$.

Note: The US driver’s license is much more than just a license. It’s commonly used for age checks and serves as a valid travel document for domestic flights. Starting October 1st, 2020, boarding domestic flights requires the ownership of a REAL ID driver’s license or ID card. More info on this can be found here.

Finding a job

Even though becoming a US resident doesn’t mean you have to work for a US company or work at all but I assume that most of us need to make a living somehow so getting a job was one of my major concerns. Unfortunately, there’s no general rule of thumb I can provide you with because finding a job is strongly dependent on the industry you are operating in. For me applying through i.e. glassdoor wasn’t successful at all but instead going to Meetups and meeting people in person worked pretty well. I read that only a fraction of available jobs is being posted online and networking is so much more important than I was used to it from Europe. In general, I’d suggest at least 3 months in which one can settle and interview with a lot of companies to find a good job.

Travel

The article is getting quite long already but this is an important part, so bear with me. A big topic is travelling internationally as a resident as you’re supposed to be in the US. Nonetheless, having vacation and staying away for a couple of weeks is usually not an issue. If you can provide documentation that you’re actually living in the US and justify your trip, there’s nothing to worry about even with frequent travels. But if you plan on staying away longer than one year then you must apply for a reentry permit as your permanent resident card becomes technically invalid after this period.

A topic I haven’t covered here is the process of naturalization which means becoming a citizen. As I have yet to explore this myself, I can only point you to the official website.

This was the last part on the topic for now, please reach out if you have any questions or remarks!

Tags: diversity visa, green card, lottery, USA