A file, like anything else in a computer, is at its core a sequence of bits — data. Programs read or write files by accessing or modifying that sequence. They infer the meaning of the data from the context in which it was read.
What we typically think of as files is simply data stored on a persistent disk. However, since files are just sequences of bits, there is no reason for them to be limited to data stored somewhere persistent. They could be just a block of data in RAM. They could be data read off the wire from the network. They could be anything! Or, if you design it to be, everything.
Pretty cool! In fact, if we treat everything as a file, it allows us to solve problems in consistent and simple ways. Take for example a problem I have been toying with–the problem of Kubernetes secrets:
- I want to expose a secret (a token) to my Kubernetes pod.
- Possession of the token is sufficient to gain access to a protected system.
- Only the pod I’m creating should have access to the secret.
- The pod accesses the secret (as a file!) by mounting it.
- I may want to change the secret in the future.
- To decrease the chance of the token leaking, I don’t want the token or the secret dumped to the screen or written to the disk.
Some might use sops for static secrets (as we showed in Safely Secure Secrets: a SOPS plugin for kustomize), but I have a nifty API to generate new ones on demand. I want to use that with as little fuss as possible. So, I need a strategy to create my secret using that API without violating the above constraints.
Kubernetes provides a few methods to create secrets. The one most comfortable for those who’ve used Kubernetes for a while is likely to be creating the secret from a yaml document, probably stored on disk. That’s easy, but I’d have to jump through some hoops to satisfy my requirement that I don’t dump the token to the screen or write it to disk.
Another method provided by Kubernetes uses kubectl — a CLI for tickling the Kubernetes API — directly. You can create secrets with kubectl by reading the value from a file, or by passing the value on the command line. For example:
kubectl create secret -n secret-place generic token \ --from-literal=token-file=secret-value
The above command creates a secret named token in the namespace secret-place. When mounted, it will provide a file named token-file with the literal value “secret-value”. This is great, except for the fact that I’d need to copy and paste the token onto the command line. Or, I could simply pass it in as the result of a sub-shell. Let’s try that.
Note for that this example I create two functions in bash which return different values. In reality, I would probably use curl or some SDK to get the token.
$ kubectl create secret -n secret-place generic token \ --from-literal=token-value=$(get_secret) secret/token created $ kubectl create secret -n secret-place generic token \ --from-literal=token-file=$(get_new_secret) Error from server (AlreadyExists): secrets "token" already exists
Oh. I can’t create the secret twice. sadface. I refuse to be defeated, though. Valiantly, I check the help for the command, which mentions the following two parameters.
--dry-run=false: If true, only print the object that would be sent, without sending it. -o, --output='': Output format. One of: json|yaml|name|go-template|go-template-file|template|templatefile|jsonpath|jsonpath-file.
Passing –dry-run-false will give me the secret without creating it. Passing –o=yaml will output the secret to the console in a format that kubectl apply will accept. kubectl apply allows updates to resources! I may just have my solution. Here is where everything being a file shines.
stdin is the name given to the file in which a program reads its input. For example, when you type into your shell, your shell is reading the characters you write from the file stdin. Kubectl can read input to kubectl apply from stdin by telling it to read from a special file named –. So, let’s see how it all works!
$ kubectl create secret -n secret-place generic token \ --from-literal=token-file=$(get_secret) --dry-run=true -o=yaml | kubectl apply -f - secret/token created $ kubectl create secret -n secret-place generic token \ --from-literal=token-file=$(get_new_secret) --dry-run=true \ -o=yaml | kubectl apply -f - secret/token configured
Great success! You may notice that I used another Unix technique, pipes, to pass the information from the output of create secret to the input of apply without the secret ever being displayed or written to a file. This is a very powerful technique that likely deserves its own post. That said, I can improve my commands even further by making use of pipes end-to-end.
$ get_secret | kubectl create secret -n secret-place \ generic token --from-file=token-file=/dev/stdin \ --dry-run=true -o=yaml | kubectl apply -f - secret/token created $ get_new_secret | kubectl create secret -n secret-place \ generic token --from-file=token-file=/dev/stdin \ --dry-run=true -o=yaml | kubectl apply -f - secret/token configured
How does it improve things? Well, besides using pipes, the contents of the secret passed on the command line may be seen by inspecting the output of the command (e.g. via ps, /proc/$pid/cmdline, etc.). Using pipes lets us pass the data end-to-end through a chain of inputs and outputs without it ever being leaked.
Note that in the example I have changed –from-literal to –from-file: I have instructed kubectl to read stdin to get the contents of the secret. By making use of pipelines, and yet again treating the standard input to the program as a file, I can update my secrets with a simple chain of commands:
- Get the value.
- Write the value to stdout.
- Read the value from stdin.
- Output the value into a secret document yaml format.
- Apply the value to the kubernetes API.
This achieves all my goals, while being quite simple.
I’m happy. Thanks, Unix!