Skip to content

Commit

Permalink
Remove creds-init intContainer
Browse files Browse the repository at this point in the history
Creds-init is an initContainer that writes credential files (like .ssh) to
/tekton/home. One side-effect of relying on an initContainer to write these
files is that they receive the UID / Group ID of the initContainer's process.
This side-effect breaks any Step in the Task that relies on these credentials
but runs with a different UID. An example of where this can happen is on
OpenShift, where the UID of the user is randomized for each container in
order to limit the fallout of malicious process breaking out.

This commit removes the creds-init initContainer from all TaskRun Pods. I
haven't removed the creds-init binary from our build process in this
changeset. Doing so generates a lot of extra line noise which distracts
from the core modification being presented here.

This commit introduces an example YAML that successfully exercises
creds-init with vanilla git commands both when the
disable-home-env-overwrite flag is "false" (the current default)
and when it's "true". In addition the example demonstrates working
with creds-init credentials when a non-root securityContext is set
on a Step.
  • Loading branch information
Scott authored and tekton-robot committed Jul 2, 2020
1 parent f01b977 commit bbb767c
Show file tree
Hide file tree
Showing 22 changed files with 681 additions and 322 deletions.
3 changes: 2 additions & 1 deletion cmd/creds-init/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package main
import (
"flag"

"github.com/tektoncd/pipeline/pkg/apis/pipeline"
"github.com/tektoncd/pipeline/pkg/credentials"
"github.com/tektoncd/pipeline/pkg/credentials/dockercreds"
"github.com/tektoncd/pipeline/pkg/credentials/gitcreds"
Expand All @@ -37,7 +38,7 @@ func main() {

builders := []credentials.Builder{dockercreds.NewBuilder(), gitcreds.NewBuilder()}
for _, c := range builders {
if err := c.Write(); err != nil {
if err := c.Write(pipeline.CredsDir); err != nil {
logger.Fatalf("Error initializing credentials: %v", err)
}
}
Expand Down
18 changes: 17 additions & 1 deletion cmd/entrypoint/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import (
"time"

"github.com/tektoncd/pipeline/pkg/credentials"
"github.com/tektoncd/pipeline/pkg/credentials/dockercreds"
"github.com/tektoncd/pipeline/pkg/credentials/gitcreds"
"github.com/tektoncd/pipeline/pkg/entrypoint"
"github.com/tektoncd/pipeline/pkg/termination"
)
Expand All @@ -41,8 +43,22 @@ var (
)

func main() {
// Add credential flags originally used in creds-init.
gitcreds.AddFlags(flag.CommandLine)
dockercreds.AddFlags(flag.CommandLine)

flag.Parse()

// Copy creds-init credentials from secret volume mounts to /tekton/creds
// This is done to support the expansion of a variable, $(credentials.path), that
// resolves to a single place with all the stored credentials.
builders := []credentials.Builder{dockercreds.NewBuilder(), gitcreds.NewBuilder()}
for _, c := range builders {
if err := c.Write("/tekton/creds"); err != nil {
log.Printf("Error initializing credentials: %s", err)
}
}

e := entrypoint.Entrypointer{
Entrypoint: *ep,
WaitFiles: strings.Split(*waitFiles, ","),
Expand All @@ -56,7 +72,7 @@ func main() {
Results: strings.Split(*results, ","),
}

// Copy any creds injected by creds-init into the $HOME directory of the current
// Copy any creds injected by the controller into the $HOME directory of the current
// user so that they're discoverable by git / ssh.
if err := credentials.CopyCredsToHome(credentials.CredsInitCredentials); err != nil {
log.Printf("non-fatal error copying credentials: %q", err)
Expand Down
28 changes: 27 additions & 1 deletion docs/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,14 @@ This is required because while Tekton does set the $HOME environment variable
to `/tekton/home` by default, `ssh` ignores that environment variable and only
considers the user's home as that described in `/etc/passwd`.

**Note:** This additional symlink is not required if you are using the
**Note:** The additional symlink is not required if you are using the
[`git-clone` catalog Task](https://github.com/tektoncd/catalog/tree/v1beta1/git)
or Git PipelineResource.

For an example of vanilla git commands using the SSH credentials described
above, see the
[authenticating-git-commands example](../examples/v1beta1/taskruns/authenticating-git-commands.yaml).

## Basic authentication (Git)

1. Define a `Secret` containing the username and password that the `Run` should
Expand Down Expand Up @@ -375,6 +379,28 @@ Credential annotation keys must begin with `tekton.dev/docker-` or
`tekton.dev/git-`, and the value describes the URL of the host with which to use
the credential.

## Using credentials as non-root user

For a number of reasons you may need to use the credentials described in this
doc in non-root contexts:

- Your platform may randomize the user and/or groups that your containers run as.
- The Steps of Tasks that you use may define a non-root `securityContext`.
- Tasks themselves may specify non-root `securityContext`s applied to all `Steps`.

Running as a non-root user has several effects that need to be accounted for
when using the credentials mounted with the process described above:

1. Certain credential types (SSH/git) require that the user have a valid home
directory defined in `/etc/passwd`. Just having a random UID but no home directory
will result in SSH erroring out.
2. Credentials may need to be moved or symlinked from the `$HOME` directory that
Tekton defines (`/tekton/home`) to the correct `home` directory for your user.
This is true for SSH, which ignores the `$HOME` environment variable completely.

For an example of using SSH credentials in a non-root `securityContext`, see the
[authenticating-git-commands example](../examples/v1beta1/taskruns/authenticating-git-commands.yaml).

## Implementation details

### Docker `basic-auth`
Expand Down
2 changes: 1 addition & 1 deletion docs/variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ This page documents the variable substitions supported by `Tasks` and `Pipelines
| `workspaces.<workspaceName>.path` | The path to the mounted `Workspace`. |
| `workspaces.<workspaceName>.claim` | The name of the `PersistentVolumeClaim` specified as a volume source for the `Workspace`. Empty string for other volume types. |
| `workspaces.<workspaceName>.volume` | The name of the volume populating the `Workspace`. |
| `credentials.path` | The path to the credentials written by the `creds-init` init container. |
| `credentials.path` | The path to credentials injected from Secrets with matching annotations. |
| `context.taskRun.name` | The name of the `TaskRun` that this `Task` is running in. |
| `context.task.name` | The name of this `Task`. |

Expand Down
204 changes: 204 additions & 0 deletions examples/v1beta1/taskruns/authenticating-git-commands.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
# This example demonstrates usage of creds-init credentials to issue
# git commands without a Git PipelineResource or git-clone catalog task.
#
# In order to exercise creds-init a sidecar is used to run a
# git server fronted by SSH. The sidecar does the following things:
# - Generates a host key pair, providing the public key to Steps for their known_hosts file
# - Accepts a public key generated from creds-init credentials and uses that for an authorized_keys file
# - Creates a bare git repo for the test git commands to run against
# - Starts sshd and tails its log, waiting for the git commands to come in over SSH
#
# Two separate Steps then perform authenticated git actions against the sidecar
# git server using the credentials mounted by creds-init:

# The first step makes a git clone of the bare repository and populates it
# with a file.
#
# The second step makes a git clone of the populated repository and checks
# the contents of the repo match expectations. This step runs as a non-root
# user in order to exercise creds-init credentials when a securityContext
# is set.
#
# Notice that in each Step there is different code for handling creds-init
# credentials when the disable-home-env-overwrite flag is "false" and when
# it's "true".
apiVersion: v1
kind: Secret
type: kubernetes.io/ssh-auth
metadata:
name: ssh-key-for-git
annotations:
tekton.dev/git-0: localhost
data:
# This key was generated for this test and isn't used for anything else.
ssh-privatekey: LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUJsd0FBQUFkemMyZ3RjbgpOaEFBQUFBd0VBQVFBQUFZRUF5T1g3ZG5OWlFBZVk4cHNMOXlaUnp3NXNDVG1yWGh6Zld1YTZuZ2VDQ0VRRTY4YjVUSThTCkNlbEhlNG9oTUtBdXZ0ZTE4YXJMK2EvVldpeFN6a2tBMmFIZVhkdUJ1bStkS2R2TlVVSUhNc1dOUythcENQYmE4R3ZGaHYKdG81Tkx0bWpxT2M0WjJkK1RPS3AvakMrS3pvUDFHQWdRL25QMitMTldabzlvTTc4TzQ3Z1dSem9FNlFKeGJqbFlPMHRMbwp4YXUxdTNrbUtsNSthbUxsNHpGN25wdmV1dGlSWDhmY2hGam5Ka2dqK3BVeFJvTGF4SDdDN0NTcDExWUkyMEhKRVFXeEk3CllaekNTYml5KzZ1a2l0Tk1MZ29qMnpSTGl2ZTVvZm9nenpYbkdWUUpZdUIzOFhQM0ZIQWMvOXhzUXdzd3dQS2hkQ3g4T0QKbjErYXpLOHp5SGhXK0dxckJhS1R4cDlrcVRpKzZSMWk4ZjVxOEt6NXpGVTZmd05qQXZ3STFBZ3IwS2FzU1JxWTVMcGxnTgpZcW1DY01JODZKUnRGWHRWWVQrT05tdWFhYUQ1QUErbnpkNW81R0haZTlFSlNqUThZMHZwbjhmNjNjeEw2RTdzVmxpMnpzCnNhN1RST2JMK3YyVnFuSlpEY2pIZXMzS1M5Mld0V3RJbXdXOG81VkRBQUFGaU04K0NUL1BQZ2svQUFBQUIzTnphQzF5YzIKRUFBQUdCQU1qbCszWnpXVUFIbVBLYkMvY21VYzhPYkFrNXExNGMzMXJtdXA0SGdnaEVCT3ZHK1V5UEVnbnBSM3VLSVRDZwpMcjdYdGZHcXkvbXYxVm9zVXM1SkFObWgzbDNiZ2Jwdm5TbmJ6VkZDQnpMRmpVdm1xUWoyMnZCcnhZYjdhT1RTN1pvNmpuCk9HZG5ma3ppcWY0d3ZpczZEOVJnSUVQNXo5dml6Vm1hUGFETy9EdU80RmtjNkJPa0NjVzQ1V0R0TFM2TVdydGJ0NUppcGUKZm1waTVlTXhlNTZiM3JyWWtWL0gzSVJZNXlaSUkvcVZNVWFDMnNSK3d1d2txZGRXQ050QnlSRUZzU08yR2N3a200c3Z1cgpwSXJUVEM0S0k5czBTNHIzdWFINklNODE1eGxVQ1dMZ2QvRno5eFJ3SFAvY2JFTUxNTUR5b1hRc2ZEZzU5Zm1zeXZNOGg0ClZ2aHFxd1dpazhhZlpLazR2dWtkWXZIK2F2Q3MrY3hWT244RFl3TDhDTlFJSzlDbXJFa2FtT1M2WllEV0twZ25EQ1BPaVUKYlJWN1ZXRS9qalpybW1tZytRQVBwODNlYU9SaDJYdlJDVW8wUEdOTDZaL0grdDNNUytoTzdGWll0czdMR3UwMFRteS9yOQpsYXB5V1EzSXgzck55a3ZkbHJWclNKc0Z2S09WUXdBQUFBTUJBQUVBQUFHQUNQSGtmbU9vWjZkdThlNWhYQUhDeHJ0WHFCCmwvUGROL1JtYmJqRW05U216czR5cWEwd1BUdzhrMU81VHM0V05nY1hMZFVRTlB6YkE4aWFWTGtvL0JqKzhiSFlhMmdmeVMKUE5qaWpXbXBOR09EWlF2Q0h2b095WUdpNjkycHovWnNTZCt0bEFzNm54LzY1ZjcwZHdVREJub0FjZnFLY28wQnVMRlNBKworamY5RnhISGYzQkFEUS9TdDVFQjlZelo1Q2F2cTRQcjZvS2w3R3RpbnRIbTZIbUlwTUlubWVEMnV3cjl2ZGZ1RGJhVDdYClVOSm10elVGck1uOUhlOWd1WkoyTXd3a015S09ScnRhVFA3VjFZK3FOM3ZncStmRkNtU0VkekxBU3BWTHMyL1hQTCtwTnAKTTVZUVRRMFJSZWdKTEdtTHZ0ZmpkK1RRMFQ0bjBucnBJVGRXRTRsL05sTG9taTVhUndzQXFtY2hZSGxhN0g3YlNyS2lKawpyWTg1RTliZm8wSXJqUDNQNzFYNmxjcTB0VDhDTklUQUNleWJQT3kwcDVDc3JwZTdhZEJBOXF4MTZjR2tkZ0NPWk9GMnRpCktoWWJHeTc4ei9YNEh2OEptVmhaSHF3RlFQQzVleWljbE1PTFJXNDJOcUJhNEVFc3RHT3l4MHZwa0lVS3VhRlJuQkFBQUEKd1FESytXYzU1WHVpWjgySXM5NnN2bWIrR0Y5c2pBRWVaWHpHSWpDL1NHVEhIWTZSQmc1TnlQOUdZNmtoWnBjd0cyTU52dQpZUjhuN0psRWlVanU2cjY2Smh0WGtvdTR4WlU1dDkvMlJvdHRmeWpKODJmYm8yTHZmNERUOVNvSURxZnk5VmlMSjhSWUNkCkt6NnpYSHFTZ1RBRU1vaUhjbFpIZzRqTitrOW1ma2tPMDBQbEJJaE1YU0ZMLzUrZUhGdStQTmxaU1g5NUlMRjJvZ0Y1RG0KWTFuaTRUOGJjdzY2dmFzamthcjFZekptM1VidEVnSzQ2VVllNGJac2NXbWt4dngwMEFBQURCQVA4UysyTmtheWkvb3NzVApTQXpJMi9QU2tJMDVEY1lTYnNOQjZ4a3pobzdKaDlHeUNvbW5xZ1IxR2ZBOTBqV3AxVks0TG43TmtYYWJaVmJPc0xoT21DCkdBbVRZTHRjaTB0bkhhYk5HTEZ3ZmdiUitqRzZNQ2p4cEh5anM5MDlKSHhtYmswbElpczdPN1N3VThERGcrSEVxc0EvNUoKQ1VMTWU3em9mNERhUnZXdFhTRks2ZW5LNnpGaHBINjVQY29TN0o0NjJhNzdUMDVGQXhMemNaRkc5VWZ5WUdMa1ZmdHRTTApNVDhudW9LaW5XTGNLSlVQeis1MjJlM3lIcis4c3pVUUFBQU1FQXlhQ28xSnRBcjRpczY0YTBuZTFUV0o0dXcyT3FDdUlDCm9acG1QN2UyRnh3bVRCSWMrbzZkSEVNVHo2c2ZZSkFxU2l4ZzYydXYzWlRTc25STWljaDZ0b1k0SVI4cWFMa1prLzU5cmEKQWFONFlvTkdpQTZxY0Jzc3NLMmZuM2YxRFJhckxPbWZHTnpTMU41S1RvSFVlUkVGWDExdHVNM1pqOGxTelFBOWZSakk1OQpFWmFnOWJaOXRJOEg5dmEvTGRMK3U3dTZZWkVRSEJCS1MxMW1tOVVXd1pDMkdUV3ZnNzRlTnRmemtZeDQxdlhIeTZBbW9ECmxuOHo2N3lvWEZzbEpUQUFBQURuTmpiM1IwUUcxbFkyZ3ViR0Z1QVFJREJBPT0KLS0tLS1FTkQgT1BFTlNTSCBQUklWQVRFIEtFWS0tLS0tCg==
# This known_hosts file doesn't actually get used; we overwrite it with the public key
# of our temporary test git server. But it's required here because otherwise creds-init
# calls ssh-keyscan which in turn tries to reach out over the network, will fail to make
# contact with the localhost SSH server because it isn't running yet, and the TaskRun
# will end in failure.
known_hosts: Cg==
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: ssh-key-service-account
secrets:
- name: ssh-key-for-git
---
apiVersion: tekton.dev/v1beta1
kind: TaskRun
metadata:
name: authenticating-git-commands
spec:
serviceAccountName: ssh-key-service-account
taskSpec:
volumes:
- name: messages
emptyDir: {}
sidecars:
- name: server
image: alpine/git:v2.24.3
securityContext:
runAsUser: 0
volumeMounts:
- name: messages
mountPath: /messages
script: |
#!/usr/bin/env ash
# Generate a private host key and give the Steps access to its public
# key for their known_hosts file.
ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key
chmod 0600 /etc/ssh/ssh_host_rsa_key*
HOST_PUBLIC_KEY=$(cat /etc/ssh/ssh_host_rsa_key.pub | awk '{ print $2 }')
echo "localhost ssh-rsa $HOST_PUBLIC_KEY" > /messages/known_hosts
# Wait for a Step to supply the server a public key generated from creds-init
# credentials.
while [ ! -f /messages/authorized_keys ] ; do
sleep 1
done
# Allow Steps to SSH login as root to this server.
mkdir /root/.ssh
cp /messages/authorized_keys /root/.ssh/
# "Unlock" the root account, allowing SSH login to succeed.
sed -i s/root:!/"root:*"/g /etc/shadow
# Create the git repo we're going to test against.
cd /root/
mkdir repo
cd repo
git init . --bare
# Start the sshd server.
/usr/sbin/sshd -E /var/log/sshd
touch /messages/sshd-ready
tail -f /var/log/sshd
steps:
- name: setup
# This Step is only necessary as part of the test, it's not something you'll
# ever need in a real-world scenario involving an external git repo.
image: alpine/git:v2.24.3
securityContext:
runAsUser: 0
volumeMounts:
- name: messages
mountPath: /messages
script: |
#!/usr/bin/env ash
# Generate authorized_keys file from the creds-init private key and give
# it to the sidecar server so that Steps can successfully SSH login
# using creds-init credentials.
ssh-keygen -y -f $(credentials.path)/.ssh/id_ssh-key-for-git > /messages/authorized_keys
# Wait for sshd to start on the git server.
while [ ! -f /messages/sshd-ready ] ; do
sleep 1
done
- name: git-clone-and-push
image: alpine/git:v2.24.3
securityContext:
runAsUser: 0
workingDir: /root
volumeMounts:
- name: messages
mountPath: /messages
script: |
#!/usr/bin/env ash
set -xe
if [ -d /tekton/home/.ssh ] ; then
# When disable-home-env-overwrite is "false", creds-init credentials
# will be copied to /tekton/home/.ssh by the entrypoint. But we need
# them in /root/.ssh.
# Overwrite the creds-init known_hosts file with that of our test
# git server. You wouldn't need to do this in any kind of real-world
# scenario involving an external git repo.
cp /messages/known_hosts $(credentials.path)/.ssh/
# Symlink /tekton/creds/.ssh to /root/.ssh because this script issues
# vanilla git commands of its own. Git PipelineResources and the git-clone
# catalog task handle this for you.
ln -s $(credentials.path)/.ssh /root/.ssh
else
# When disable-home-env-overwrite is "true", creds-init credentials
# will be copied to /root/.ssh by the entrypoint. We just need to
# overwrite the known_hosts file with that of our test git server.
cp /messages/known_hosts /root/.ssh/known_hosts
fi
git clone root@localhost:/root/repo ./repo
cd repo
git config user.email "example@example.com"
git config user.name "Example"
echo "Hello, world!" > README
git add README
git commit -m "Test commit!"
git push origin master
- name: git-clone-and-check
image: gcr.io/tekton-releases/dogfooding/alpine-git-nonroot:mario
# Because this Step runs with a non-root security context, the creds-init
# credentials will fail to copy into /tekton/home. This happens because
# our previous step _already_ wrote to /tekton/home and ran as a root
# user. So there will be warning messages reporting "unsuccessful cred
# copy". These can be safely ignored and instead this Step will copy
# the credentials out of /tekton/creds to nonroot's HOME directory.
securityContext:
runAsUser: 1000
workingDir: /home/nonroot
volumeMounts:
- name: messages
mountPath: /messages
script: |
#!/usr/bin/env ash
set -xe
if [ -d /tekton/home/.ssh ] ; then
# When disable-home-env-overwrite is "false", creds-init credentials
# will be copied to /tekton/home/.ssh by the entrypoint. But we need
# them in /home/nonroot/.ssh.
# Overwrite the creds-init known_hosts file with that of our test
# git server. You wouldn't need to do this in any kind of real-world
# scenario involving an external git repo.
cp /messages/known_hosts $(credentials.path)/.ssh/
# Symlink /tekton/creds/.ssh to /home/nonroot/.ssh because this script issues
# vanilla git commands of its own and we're running as a non-root user.
# Git PipelineResources and the git-clone catalog task handle this for you.
ln -s $(credentials.path)/.ssh /home/nonroot/.ssh
else
# When disable-home-env-overwrite is "true", creds-init credentials
# will be copied to /home/nonroot/.ssh by the entrypoint. We just need to
# overwrite the known_hosts file with that of our test git server.
cp /messages/known_hosts /home/nonroot/ssh/known_hosts
fi
git clone root@localhost:/root/repo ./repo
cd repo
cat README | grep "Hello, world!"
2 changes: 2 additions & 0 deletions pkg/apis/pipeline/paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ const (
DefaultResultPath = "/tekton/results"
// HomeDir is the HOME directory of PipelineResources
HomeDir = "/tekton/home"
// CredsDir is the directory where credentials are placed to meet the creds-init contract
CredsDir = "/tekton/creds"
)
13 changes: 7 additions & 6 deletions pkg/credentials/dockercreds/creds.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,18 @@ var config basicDocker
var dockerConfig string
var dockerCfg string

// AddFlags adds CLI flags that dockercreds supports to a given flag.FlagSet.
func AddFlags(flagSet *flag.FlagSet) {
flags(flagSet)
}

func flags(fs *flag.FlagSet) {
config = basicDocker{make(map[string]entry)}
fs.Var(&config, "basic-docker", "List of secret=url pairs.")
fs.StringVar(&dockerConfig, "docker-config", "", "Docker config.json secret file.")
fs.StringVar(&dockerCfg, "docker-cfg", "", "Docker .dockercfg secret file.")
}

func init() {
flags(flag.CommandLine)
}

// As the flag is read, this status is populated.
// basicDocker implements flag.Value
type basicDocker struct {
Expand Down Expand Up @@ -148,8 +149,8 @@ func (*basicDockerBuilder) MatchingAnnotations(secret *corev1.Secret) []string {
return flags
}

func (*basicDockerBuilder) Write() error {
dockerDir := filepath.Join(os.Getenv("HOME"), ".docker")
func (*basicDockerBuilder) Write(directory string) error {
dockerDir := filepath.Join(directory, ".docker")
basicDocker := filepath.Join(dockerDir, "config.json")
if err := os.MkdirAll(dockerDir, os.ModePerm); err != nil {
return err
Expand Down
Loading

0 comments on commit bbb767c

Please sign in to comment.