Add some thoughts on what credential handling should look like

This commit is contained in:
R Tyler Croy 2020-10-28 20:55:25 -07:00
parent 03fea4a919
commit b45ccf859a
No known key found for this signature in database
GPG Key ID: E5C92681BEF6CEA2
3 changed files with 261 additions and 1 deletions

View File

@ -20,7 +20,7 @@
<!-- CSS -->
<link rel="stylesheet" href="{{ "/assets/css/main.css" | relative_url }}">
<link rel="stylesheet" href="{{ "/assets/css/syntax.css" | relative_url }}">
<link rel="stylesheet" href="{{ "/assets/css/github-syntax.css" | relative_url }}">
<!-- SEO Plugin -->
{% seo %}

View File

@ -0,0 +1,187 @@
---
layout: post
title: "Passing credentials to Otto steps"
tags:
- otto
- security
---
One of the major problems I want to solve with
[Otto](https://github.com/rtyler/otto) is that in many CI/CD tools secrets and
credentials can be inadvertently leaked. Finding a way to allow for the secure
use of credentials without giving developers direct access to the secrets is
something _most_ CI/CD systems fail at today. My hope is that Otto will succeed
because this is a problem being considered from the beginning. In this post,
I'm going to share some of the thoughts I currently have on how Otto can pass
credentials around while removing or minimizing the possibility for them to be
leaked by user code.
In the course of my career I have spent a **lot** of time on this problem
already. In the [Jenkins project](https://jenkins.io) we have had to deal with
a number of "credential integrity" problems where plugins, or more frequently
users' pipelines end up leaking credentials. I have some thoughts about this
problem with Jenkins written down already in these posts:
* [Thoughts about a secure enclave for Jenkins Pipeline](/2019/04/30/pipeline-secure-enclave.html), musing on how Jenkins could improve or otherwise restrict credentials access.
* [Jenkins should not be the only line of defense](/2019/04/15/trust-and-jenkins.html), noting the importance of defense in depth and siloing "build" from "deploy" after a high profile breach via an open source project's CI system.
* [It's not stealing when you're giving them
away](/2019/02/22/its-not-credentials-stealing.html), outlining the key
problem with exposing credentials to user-defined code in a pipeline. Choice
quote: "_fundamentally, exposing credentials to user-defined code will always
have the possibility for disclosure._"
* [It's no secret where GitOps falls down](/2018/08/14/gitops-and-secrets.html), discussing the challenges of GitOps and secrets management when "everything is code."
Ultimately, the key challenge is drawing the boundary between "arbitrary
user-defined code" and credentials. Consider an open source project which
accepts pull requests and uses their CI system to validate proposed changes.
In this contrived example, pretend that the CI scripting for the project will:
* Build the package
* Run the tests
* Push a snapshot binary _somewhere_ for any manual testing that may be desired.
Presumably only the last step requires any type of credential. For
demonstration, I will use Jenkins Pipeline but the problem is present in many
other systems:
```groovy
sh 'make package'
sh 'make tests'
withCredentials([string(credentialsId: 'mytoken', variable: 'TOKEN')]) {
sh 'make deploy'
}
```
There are various mechanisms to prevent a pull request from modifying the
`Jenkinsfile` (in this example), and also ways to prevent outputting the
`mytoken` credentials into the log stream. Ultimately however, since the
credential is being exposed as an _environment variable_ which is then being
made available to user-defined code, in this case a `Makefile`. The only two ways to prevent a malicious pull request from using the token would be to:
* Not run the `make deploy` script for pull requests
* Only use the `main` branch version of the `Makefile` when running the pull request. (_note_: Jenkins basically does a variant of this by default with the `Jenkinsfile`)
My thinking right now is that **Otto should never expose credentials as
environment variables**.
## How this could work
In [this blog post](/2019/02/22/its-not-credentials-stealing.html) I alluded to the solution I am thinking about for Otto:
> The simplest [pattern], the one which exposes those credentials to user-defined
> processes is fundamentally flawed. In Jenkins, an administrator can define
> credentials when configuring some plugins such as the Slack Notification
> plugin. In the case of Slack, the user cannot access the Slack credentials, but
> can invoke `slackSend` from within their Jenkins Pipeline. I strongly believe
> that variations on this pattern, wherein **an administrator defines a credential
> and only allows administrator-governed code to utilize that credential, are the
> only ways in which credentials exposure can be avoided** (barring bugs in the
> code, etc).
(_added emphasis_)
This is effectively _exactly_ what I am thinking for Otto: credentials can
never be accessed as environment variables or by user-defined steps. Instead
they can be accessed from within the context of an administrator approved step
library.
Otto's notion of [step libraries](/2020/10/18/otto-steps.html) means that there
is _plenty_ of room for both administrator and user-defined code, but Otto
should always know where a step comes from, and that coupled with a credential
access pattern _should_ provide sufficient safety.
Using the above example, imagine that `make deploy` is essentially just running:
```bash
curl -H "Token: $TOKEN" -X PUT -d @artifact-20201028-312.tar.gz https://example.com/api/archive
```
In Otto, that code would need to be wrapped in a step library of some form, for
example a `deploy-snapshot` step.
The step below is a sketch in Ruby for ease of understanding:
```ruby
#!/usr/bin/env ruby
require 'json'
require 'yaml'
# Imagine that this gem provides the agent control socket shim
require 'otto/control'
# Load the invocation file which defines where the control socket lives
invoke_data = YAML.load(File.read(ARGV.first))
control = invoke_data['configuration']['ipc']
artifact = invoke_data['parameters]['artifact']
# Send the HTTP request for the credential
response = Otto::Control::send(
{:type => :RetrieveCredential,
:name => 'mytoken'})
exit 1 unless response.ok?
# Grab the token to use
token = response['mytoken']['value']
# This could be done with Net::HTTP, but just for symmetry sake
# shell out to curl
system("curl -H 'Token: #{token}' -X PUT -d @#{artifact} https://example.com/api/archive")
```
The above would then be packaged as a `deploy-snapshot-<version>.tar.gz` and
uploaded to the wherever Otto stores its step libraries (yet to be defined).
In order for this step to work properly, an administrator would need to **explicitly**
state that the `deploy-snapshot` can access:
* Credentials of specific names, e.g. `mytoken`.
* All credentials.
In the above example, if `mytoken` was explicitly granted, any other credential
retrieval requests would be denied and the step would error out since it did
not receive the credential.
I can imagine somebody granting "all credentials" access to a standard library step such as `sh`. I believe in this case the credentials would still be safe so long as:
* User-defined code cannot locate the agent control socket.
* User-defined code cannot interact with the agent control socket. Right now
there's not an authentication scheme applied to the agent/step control
socket, but it is trivial to add a pre-shared secret that is given to each
step's internal machinery to use for accessing the socket.
There may also be cases where an administrator is comfortable with allowing a
user-defined step, such as one that is located in the local source tree or at a
remote URL (e.g. on GitHub). For these cases, I think the initial behavior has
to be a "deny and record the request." The administrator should then be able to
approve a user-defined step for credentials access at a specified revision such
as `deploy-snapshot@main` or `deploy-snapshot@1ff9033`.
## How this could not work
The reason environment variables are frequently used for passing secrets around
is because it's _easy_. Well, not only that. Developers are _lazy_. The
forceful demarcation line between which code does and does not have credential
access hasn't been validated with lazy developers other than myself. I don't
yet know whether the bar I am setting is too high, or just high enough for
real-world usage.
If the barrier to entry is too high, I can easily imagine somebody writing a
`withSecrets` step which basically just takes whatever secrets you want and
dumps them into environment variables. This would still require an
administrator to approve the step, but the system would still allow the
`withSecrets` step code to retrieve credentials and then handle them in any
insecure manner they please.
---
I believe that the approach I describe above will prevent the easy and foolish
cases where credentials get leaked. Ultimately, I don't think Otto can 100%
prevent credential leakage by somebody determined to shoot themselves in the
foot.
If you're a pipeline user or CI/CD systems administrator and have thoughts on
how you would want to use or lock-down credentials, please join `#otto` on
Freenode or [email me](/about) with your thoughts!

View File

@ -0,0 +1,73 @@
/*
* GitHub style for Pygments syntax highlighter, for use with Jekyll
* Courtesy of GitHub.com
*/
pre{
/* white-space: pre-wrap; */
overflow-x: auto;
}
.highlight pre, pre, .highlight .hll { font-size: 1rem; background-color: #f8f8f8; border: 1px solid #ccc; padding: 6px 10px; border-radius: 3px; }
.highlight .c { color: #999988; font-style: italic; }
.highlight .err { color: #a61717; background-color: #e3d2d2; }
.highlight .k { font-weight: bold; }
.highlight .o { font-weight: bold; }
.highlight .cm { color: #999988; font-style: italic; }
.highlight .cp { color: #999999; font-weight: bold; }
.highlight .c1 { color: #999988; font-style: italic; }
.highlight .cs { color: #999999; font-weight: bold; font-style: italic; }
.highlight .gd { color: #000000; background-color: #ffdddd; }
.highlight .gd .x { color: #000000; background-color: #ffaaaa; }
.highlight .ge { font-style: italic; }
.highlight .gr { color: #aa0000; }
.highlight .gh { color: #999999; }
.highlight .gi { color: #000000; background-color: #ddffdd; }
.highlight .gi .x { color: #000000; background-color: #aaffaa; }
.highlight .go { color: #888888; }
.highlight .gp { color: #555555; }
.highlight .gs { font-weight: bold; }
.highlight .gu { color: #800080; font-weight: bold; }
.highlight .gt { color: #aa0000; }
.highlight .kc { font-weight: bold; }
.highlight .kd { font-weight: bold; }
.highlight .kn { font-weight: bold; }
.highlight .kp { font-weight: bold; }
.highlight .kr { font-weight: bold; }
.highlight .kt { color: #445588; font-weight: bold; }
.highlight .m { color: #009999; }
.highlight .s { color: #dd1144; }
.highlight .n { color: #333333; }
.highlight .na { color: teal; }
.highlight .nb { color: #0086b3; }
.highlight .nc { color: #445588; font-weight: bold; }
.highlight .no { color: teal; }
.highlight .ni { color: purple; }
.highlight .ne { color: #990000; font-weight: bold; }
.highlight .nf { color: #990000; font-weight: bold; }
.highlight .nn { color: #555555; }
.highlight .nt { color: navy; }
.highlight .nv { color: teal; }
.highlight .ow { font-weight: bold; }
.highlight .w { color: #bbbbbb; }
.highlight .mf { color: #009999; }
.highlight .mh { color: #009999; }
.highlight .mi { color: #009999; }
.highlight .mo { color: #009999; }
.highlight .sb { color: #dd1144; }
.highlight .sc { color: #dd1144; }
.highlight .sd { color: #dd1144; }
.highlight .s2 { color: #dd1144; }
.highlight .se { color: #dd1144; }
.highlight .sh { color: #dd1144; }
.highlight .si { color: #dd1144; }
.highlight .sx { color: #dd1144; }
.highlight .sr { color: #009926; }
.highlight .s1 { color: #dd1144; }
.highlight .ss { color: #990073; }
.highlight .bp { color: #999999; }
.highlight .vc { color: teal; }
.highlight .vg { color: teal; }
.highlight .vi { color: teal; }
.highlight .il { color: #009999; }
.highlight .gc { color: #999; background-color: #EAF2F5; }