I’ve been playing with the new credentials feature that comes with Rails 5.2 and it looks really cool! It lets you store encrypted credentials (aka “secrets”) right in the repo, and decrypt them with the master key when you need to read it.
See this blog post if you’re curious how it’s different from
secrets.yml introduced in Rails 5.1.
The approach is very similar to Shopify’s ejson, with the difference that Rails decided not to use asymmetric encryption like ejson does.
So, how does this new credentials management works with containerized Rails apps that run in Kubernetes? TL;DR it works surprisingly smooth.
There’s no rocket science in the setup, but I wrote this post to show how easy is the deployment of Rails 5.2 Credentials.
Now it’s time to build a container. First, let’s add the master key to
.dockerignore file so it doesn’t get into the container (we don’t want to expose the key to container registry).
$ echo config/master.key > .dockerignore
Let’s build the container using this minimalistic
FROM ruby:2.5 RUN mkdir -p /app WORKDIR /app ENV RAILS_ENV production ENV RAILS_SERVE_STATIC_FILES true ENV RAILS_LOG_TO_STDOUT true COPY Gemfile /app/ COPY Gemfile.lock /app/ RUN bundle config --global frozen 1 RUN bundle install --without development test COPY . /app EXPOSE 3000 CMD ["rails", "server", "-b", "0.0.0.0"]
$ docker build -t kirshatrov/secretland:v1 .
And run it with the master key as an ENV variable:
$ docker run -i -t -p 3000:3000 -e RAILS_MASTER_KEY=3bed2fdcb0261e6f48850de01a85fb5b kirshatrov/secretland:v1
If you create a silly controller to (unsafely) render secrets, you would see this output:
Don’t forget to push the container to Docker registry so Kubernetes nodes could download and run it:
$ docker push kirshatrov/secretland:v1
Before creating any Kubernetes resources, we need to create the secret (actually it’s the first time I’m using Kubernetes secrets!):
$ kubectl create secret generic secretland-secrets --from-literal=rails-master-key=3bed2fdcb0261e6f48850de01a85fb5b secret "secretland-secrets" created $ kubectl describe secret secretland-secrets Name: secretland-secrets Namespace: default Labels: <none> Annotations: <none> Type: Opaque Data ==== rails-master-key: 32 bytes
And the Deployment spec:
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: secretland labels: app: secretland spec: selector: matchLabels: app: secretland template: metadata: labels: app: secretland spec: containers: - image: kirshatrov/secretland:v1 name: rails ports: - containerPort: 3000 env: - name: RAILS_MASTER_KEY valueFrom: secretKeyRef: name: secretland-secrets key: rails-master-key
Here’s the trick: we set the ENV variable (
RAILS_MASTER_KEY) from the value of the secret that we’ve created earlier. This allows us to separate secrets from Deployments, and avoid leaking the master key to the Deployment resource. We could even push the YAML with Deployment spec to the application repo.
Updated: as suggested by Victor in the comments, it may be better to mount the secret key as a file in
config/master.key, and Rails would use that instead of the ENV variable. Here’s how the
spec.template part of the YAML would look like in this case:
spec: containers: - image: kirshatrov/secretland:v1 name: rails ports: - containerPort: 3000 volumes: - name: secrets secret: secretName: secretland-secrets items: - key: rails-master-key path: /app/config/master.key
Let’s apply the Deployment and expose it to the internet:
$ kubectl apply -f deployment.yml $ kubectl expose deployment secretland --type=LoadBalancer --port=80 --target-port=3000
Code mentioned in the post is also available as a repo.
To be honest, I haven’t expected that all these things would work so smoothly together! Credentials management in Rails 5.2 works very nicely with containerized applications, and took only a one command to push secrets to Kubernetes.
Next time I want to edit the credentials,
bin/rails credentials:edit and
git push would be enough to update them on production.