Skip to content

Commit

Permalink
auth support for ssh and tls endpoints
Browse files Browse the repository at this point in the history
Signed-off-by: CrazyMax <crazy-max@users.noreply.github.com>
  • Loading branch information
crazy-max committed Sep 19, 2022
1 parent 86f43c1 commit 4b12c06
Show file tree
Hide file tree
Showing 10 changed files with 332 additions and 19 deletions.
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -20,6 +20,7 @@ ___

* [Usage](#usage)
* [Advanced usage](#advanced-usage)
* [Authentication to a remote node](docs/advanced/auth.md)
* [Install by default](docs/advanced/install-default.md)
* [BuildKit daemon configuration](docs/advanced/buildkit-config.md)
* [Standalone mode](docs/advanced/standalone.md)
Expand Down Expand Up @@ -58,6 +59,7 @@ jobs:

## Advanced usage

* [Authentication to a remote node](docs/advanced/auth.md)
* [Install by default](docs/advanced/install-default.md)
* [BuildKit daemon configuration](docs/advanced/buildkit-config.md)
* [Standalone mode](docs/advanced/standalone.md)
Expand Down
106 changes: 106 additions & 0 deletions __tests__/auth.test.ts
@@ -0,0 +1,106 @@
import {describe, expect, jest, test, beforeEach} from '@jest/globals';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as auth from '../src/auth';

const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'docker-setup-buildx-jest')).split(path.sep).join(path.posix.sep);
const dockerConfigHome = path.join(tmpdir, '.docker');
const credsdir = path.join(dockerConfigHome, 'buildx', 'creds');

jest.spyOn(auth, 'getSSHDir').mockImplementation((): string => {
return path.join(tmpdir, '.ssh');
});

describe('setCredentials', () => {
beforeEach(() => {
process.env = Object.keys(process.env).reduce((object, key) => {
if (!key.startsWith(auth.envPrefix)) {
object[key] = process.env[key];
}
return object;
}, {});
});

// prettier-ignore
test.each([
[
'mycontext',
'docker-container',
{},
[],
[]
],
[
'docker-container://mycontainer',
'docker-container',
{},
[],
[]
],
[
'ssh://me@graviton2',
'docker-container',
{},
[],
[]
],
[
'ssh://me@graviton2',
'docker-container',
{'BUILDER_NODE_0_AUTH_SSH_PPK': 'foo'},
[path.join(credsdir, 'ssh_graviton2.ppk')],
[]
],
[
'tcp://graviton2:1234',
'remote',
{},
[],
[]
],
[
'tcp://graviton2:1234',
'remote',
{
'BUILDER_NODE_0_AUTH_TLS_CACERT': 'foo',
'BUILDER_NODE_0_AUTH_TLS_CERT': 'foo',
'BUILDER_NODE_0_AUTH_TLS_KEY': 'foo'
},
[
path.join(credsdir, 'cacert_graviton2-1234.pem'),
path.join(credsdir, 'cert_graviton2-1234.pem'),
path.join(credsdir, 'key_graviton2-1234.pem')
],
[
`cacert=${path.join(credsdir, 'cacert_graviton2-1234.pem')}`,
`cert=${path.join(credsdir, 'cert_graviton2-1234.pem')}`,
`key=${path.join(credsdir, 'key_graviton2-1234.pem')}`
]
],
[
'tcp://graviton2:1234',
'docker-container',
{
'BUILDER_NODE_0_AUTH_TLS_CACERT': 'foo',
'BUILDER_NODE_0_AUTH_TLS_CERT': 'foo',
'BUILDER_NODE_0_AUTH_TLS_KEY': 'foo'
},
[
path.join(credsdir, 'cacert_graviton2-1234.pem'),
path.join(credsdir, 'cert_graviton2-1234.pem'),
path.join(credsdir, 'key_graviton2-1234.pem')
],
[]
],
])('given %p endpoint', async (endpoint: string, driver: string, envs: Record<string, string>, expectedFiles: Array<string>, expectedOpts: Array<string>) => {
fs.mkdirSync(credsdir, {recursive: true});
for (const [key, value] of Object.entries(envs)) {
process.env[key] = value;
}
expect(auth.setCredentials(credsdir, 0, driver, endpoint)).toEqual(expectedOpts);
expectedFiles.forEach( (file) => {
expect(fs.existsSync(file)).toBe(true);
});
});
});
16 changes: 6 additions & 10 deletions __tests__/buildx.test.ts
Expand Up @@ -7,18 +7,14 @@ import * as context from '../src/context';
import * as semver from 'semver';
import * as exec from '@actions/exec';

const tmpNameSync = path.join('/tmp/.docker-setup-buildx-jest', '.tmpname-jest').split(path.sep).join(path.posix.sep);

const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'docker-setup-buildx-')).split(path.sep).join(path.posix.sep);
jest.spyOn(context, 'tmpDir').mockImplementation((): string => {
const tmpDir = path.join('/tmp/.docker-setup-buildx-jest').split(path.sep).join(path.posix.sep);
if (!fs.existsSync(tmpDir)) {
fs.mkdirSync(tmpDir, {recursive: true});
}
return tmpDir;
return tmpdir;
});

const tmpname = path.join(tmpdir, '.tmpname').split(path.sep).join(path.posix.sep);
jest.spyOn(context, 'tmpNameSync').mockImplementation((): string => {
return tmpNameSync;
return tmpname;
});

describe('isAvailable', () => {
Expand Down Expand Up @@ -136,8 +132,8 @@ describe('getConfig', () => {
config = await buildx.getConfigInline(val);
}
expect(true).toBe(!invalid);
expect(config).toEqual(`${tmpNameSync}`);
const configValue = fs.readFileSync(tmpNameSync, 'utf-8');
expect(config).toEqual(tmpname);
const configValue = fs.readFileSync(tmpname, 'utf-8');
expect(configValue).toEqual(exValue);
} catch (err) {
// eslint-disable-next-line jest/no-conditional-expect
Expand Down
9 changes: 3 additions & 6 deletions __tests__/context.test.ts
Expand Up @@ -4,16 +4,13 @@ import * as os from 'os';
import * as path from 'path';
import * as context from '../src/context';

const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'docker-setup-buildx-')).split(path.sep).join(path.posix.sep);
jest.spyOn(context, 'tmpDir').mockImplementation((): string => {
const tmpDir = path.join('/tmp/.docker-setup-buildx-jest').split(path.sep).join(path.posix.sep);
if (!fs.existsSync(tmpDir)) {
fs.mkdirSync(tmpDir, {recursive: true});
}
return tmpDir;
return tmpdir;
});

jest.spyOn(context, 'tmpNameSync').mockImplementation((): string => {
return path.join('/tmp/.docker-setup-buildx-jest', '.tmpname-jest').split(path.sep).join(path.posix.sep);
return path.join(tmpdir, '.tmpname').split(path.sep).join(path.posix.sep);
});

describe('getInputList', () => {
Expand Down
4 changes: 2 additions & 2 deletions dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

101 changes: 101 additions & 0 deletions docs/advanced/auth.md
@@ -0,0 +1,101 @@
# Authentication to a remote node

```yaml
name: ci

on:
push:

jobs:
buildx:
runs-on: ubuntu-latest
steps:
-
name: Set up SSH config
run: |
# set up SSH config and keys to connect to remote node
# in the next step
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
endpoint: ssh://me@graviton2,platforms=linux/arm64
```

In this example, we assume you know how to set up SSH config and keys in the
first step to connect to `graviton2` remote node.

To ease the integration in your workflow, we put in place environment variables
that will set up authentication for `tcp://` and `ssh://` endpoints:

## SSH authentication

To set up SSH authentication, you need to add the environment variable
`BUILDER_NODE_<idx>_AUTH_SSH_PPK` that contains the SSH private key and where
`<idx>` is the position of the node in the list of nodes.

> **Note**
>
> The index is always `0` at the moment as we don't support (yet) appending new
> nodes with this action.
With the example above it would look like this:

```yaml
name: ci

on:
push:

jobs:
buildx:
runs-on: ubuntu-latest
steps:
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
endpoint: ssh://me@graviton2,platforms=linux/arm64
env:
BUILDER_NODE_0_AUTH_SSH_PPK: ${{ secrets.GRAVITON2_SSH_PPK }}
```

## TLS authentication

You can also [set up a remote BuildKit instance](https://docs.docker.com/build/building/drivers/remote/#remote-buildkit-in-docker-container)
using the remote driver. Like the SSH authentication, you need to add the
following environment variables to set up TLS authentication with the BuildKit
client certificates:

* `BUILDER_NODE_<idx>_AUTH_TLS_CACERT`
* `BUILDER_NODE_<idx>_AUTH_TLS_CERT`
* `BUILDER_NODE_<idx>_AUTH_TLS_KEY`

Where `<idx>` is the position of the node in the list of nodes.

> **Note**
>
> The index is always `0` at the moment as we don't support (yet) appending new
> nodes with this action.
```yaml
name: ci

on:
push:

jobs:
buildx:
runs-on: ubuntu-latest
steps:
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
driver: remote
endpoint: tcp://graviton2:1234
env:
BUILDER_NODE_0_AUTH_TLS_CACERT: ${{ secrets.GRAVITON2_CA }}
BUILDER_NODE_0_AUTH_TLS_CERT: ${{ secrets.GRAVITON2_CERT }}
BUILDER_NODE_0_AUTH_TLS_KEY: ${{ secrets.GRAVITON2_KEY }}
```
91 changes: 91 additions & 0 deletions src/auth.ts
@@ -0,0 +1,91 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';

export const envPrefix = 'BUILDER_NODE';

export function setCredentials(credsdir: string, index: number, driver: string, endpoint: string): Array<string> {
let url: URL;
try {
url = new URL(endpoint);
} catch (e) {
return [];
}
switch (url.protocol) {
case 'ssh:': {
return setSSHCreds(credsdir, index, driver, url);
}
case 'tcp:': {
return setBuildKitClientCerts(credsdir, index, driver, url);
}
}
return [];
}

function setSSHCreds(credsdir: string, index: number, driver: string, endpoint: URL): Array<string> {
const driverOpts: Array<string> = [];
const sshkey = process.env[`${envPrefix}_${index}_AUTH_SSH_PPK`] || '';
if (sshkey.length == 0) {
return driverOpts;
}

const sshkeypath = `${credsdir}/ssh_${endpoint.host}.ppk`;
fs.writeFileSync(sshkeypath, sshkey);
fs.chmodSync(sshkeypath, 0o600);

const sshdir = getSSHDir();
fs.mkdirSync(sshdir, {recursive: true});

const sshconfig = `${sshdir}/config`;
fs.appendFileSync(
fs.openSync(sshconfig, 'a'),
`
Host ${endpoint.host}
IdentityFile ${sshkeypath}
ControlMaster auto
ControlPath ~/.ssh/control-%C
ControlPersist yes
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
`
);
fs.chmodSync(sshconfig, 0o600);
return driverOpts;
}

function setBuildKitClientCerts(credsdir: string, index: number, driver: string, endpoint: URL): Array<string> {
const driverOpts: Array<string> = [];
const buildkitCacert = process.env[`${envPrefix}_${index}_AUTH_TLS_CACERT`] || '';
const buildkitCert = process.env[`${envPrefix}_${index}_AUTH_TLS_CERT`] || '';
const buildkitKey = process.env[`${envPrefix}_${index}_AUTH_TLS_KEY`] || '';
if (buildkitCacert.length == 0 && buildkitCert.length == 0 && buildkitKey.length == 0) {
return driverOpts;
}
let host = endpoint.hostname;
if (endpoint.port.length > 0) {
host += `-${endpoint.port}`;
}
if (buildkitCacert.length > 0) {
const cacertpath = `${credsdir}/cacert_${host}.pem`;
fs.writeFileSync(cacertpath, buildkitCacert);
driverOpts.push(`cacert=${cacertpath}`);
}
if (buildkitCert.length > 0) {
const certpath = `${credsdir}/cert_${host}.pem`;
fs.writeFileSync(certpath, buildkitCert);
driverOpts.push(`cert=${certpath}`);
}
if (buildkitKey.length > 0) {
const keypath = `${credsdir}/key_${host}.pem`;
fs.writeFileSync(keypath, buildkitKey);
driverOpts.push(`key=${keypath}`);
}
if (driver != 'remote') {
return [];
}
return driverOpts;
}

export function getSSHDir(): string {
return path.join(os.homedir(), '.ssh');
}

0 comments on commit 4b12c06

Please sign in to comment.