Skip to content

Commit

Permalink
Authentication is now opt-in.
Browse files Browse the repository at this point in the history
Signed-off-by: Paulo Pires <pjpires@gmail.com>
  • Loading branch information
pires committed Aug 11, 2017
1 parent 5312be5 commit 66b1844
Show file tree
Hide file tree
Showing 13 changed files with 401 additions and 146 deletions.
93 changes: 80 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,67 @@
# nexus-google-iam-proxy
A proxy for authenticating Nexus Repository Manager OSS users against Google Cloud IAM.
# nexus-proxy

**Note**: If one's looking for running this software as a container, an image is made available [here](https://github.com/travelaudience/docker-nexus-google-iam-proxy).
A proxy for Nexus Repository Manager that allows for optional authentication against external identity providers.

## Introduction

While deploying Nexus Repository Manager on GKE, we identified a couple issues:

1. GCLB backend health-checks weren't working when reaching Nexus directly.
1. Couldn't expose Docker private registry with the same set-up used to
expose the other artifact repositories.

We also knew beforehand that we would need to authenticate Nexus against
[Google Cloud Identity & Access Management](https://cloud.google.com/iam/).

While the aforementioned issues were easily fixed with [Nginx](https://nginx.org/en/),
the authentication part proved much more complicated. For all of those reasons,
we decided to implement our own proxy software that would deliver everything we
needed.

While the proxy supports authentication against GCP IAM, it is disabled by default
so it can be used in simpler scenarios.

When authentication is enabled, every user attempting to access Nexus is asked to
authenticate against GCP with their GCP organization credentials. If authentication
succeeds, an encrypted token will be passed to Nexus so it knows who's logged-in.
The user can then request a different, Nexus-specific set of credentials for
using with tools like Maven, Gradle, sbt, Python (pip) and Docker.

**Note**: These Nexus-specific credentials will work for as long as the user is a member
of the organization.

**Attention**: This proxy does not manage or enforce authorization. However, it's
required that users and their roles and permissions are to be managed within
Nexus itself.

**Attention**: If one enables GCP IAM authentication, every user **must be created**
with their organization email address as the username.

## Pre-requisites

For building the project:

* JDK 8.

For basic proxying:

* A domain name configured with an `A` and a `CNAME` records pointing to the proxy.
* For local testing one may create two entries on `/etc/hosts` pointing to `127.0.0.1`.
* A running and properly configured instance of Nexus.
* One may use the default `8081` port for the HTTP connector and `5003` for the Docker registry, for example.

For opt-in authentication against Google Cloud IAM:

* All of the above.
* A GCP organization.
* A GCP project with the _Cloud Resources Manager_ API enabled.
* A set of credentials of type _OAuth Client ID_ obtained from _GCP > API Manager > Credentials_.
* Proper configuration of the resulting client with respect to the redirect URL.
* A running and properly configured instance of Nexus.
* Proper configuration of the resulting client's "_Redirect URL_".

## Generating the Keystore

The following command will generate a suitable keystore for signing JWTs:
A Java keystore is needed in order for the proxy to sign user tokens (JWT).
Here's how to generate the keystore:

```bash
$ keytool -genkey \
Expand All @@ -28,25 +75,46 @@ $ keytool -genkey \
-validity 3651
```

You will be prompted for two passwords. Please make sure they are the same. Feel free to change the value of the `dname`, `keystore` and `validity` parameters.
One will be prompted for two passwords. One must make sure the passwords match.

Also, one is free to change the value of the `dname`, `keystore` and `validity` parameters.

## Building
## Building the code

The following command will build the project and generate a runnable jar:

```bash
$ ./gradlew build
```

## Running
## Running the proxy

The following command will run the proxy on port `8080` pointing to a local
Nexus instance:
The following command will run the proxy on port `8080` with no authentication
and pointing to a local Nexus instance:

```bash
$ ALLOWED_USER_AGENTS_ON_ROOT_REGEX="GoogleHC" \
BIND_PORT="8080" \
NEXUS_DOCKER_HOST="containers.example.com" \
NEXUS_HTTP_HOST="nexus.example.com" \
NEXUS_RUT_HEADER="X-Forwarded-User" \
TLS_ENABLED="false" \
UPSTREAM_DOCKER_PORT="5000" \
UPSTREAM_HTTP_PORT="8081" \
UPSTREAM_HOST="localhost" \
java -jar ./build/libs/nexus-proxy.jar
```

## Running the proxy with GCP IAM authentication enabled

The following command will run the proxy on port `8080` with GCP IAM
authentication enabled and pointing to a local Nexus instance:

```bash
$ ALLOWED_USER_AGENTS_ON_ROOT_REGEX="GoogleHC" \
AUTH_CACHE_TTL="60000" \
BIND_PORT="8080" \
CLOUD_IAM_AUTH_ENABLED="true" \
CLIENT_ID="my-client-id" \
CLIENT_SECRET="my-client-secret" \
KEYSTORE_PATH="./.secrets/keystore.jceks" \
Expand All @@ -64,8 +132,6 @@ $ ALLOWED_USER_AGENTS_ON_ROOT_REGEX="GoogleHC" \
java -jar ./build/libs/nexus-google-iam-proxy-1.0.0.jar
```

Please check below for a description of all the supported environment variables.

## Environment Variables

| Name | Description |
Expand All @@ -75,6 +141,7 @@ Please check below for a description of all the supported environment variables.
| `BIND_PORT` | The port on which to listen for incoming requests. |
| `CLIENT_ID` | The application's client ID in _GCP / API Manager / Credentials_. |
| `CLIENT_SECRET` | The abovementioned application's client secret. |
| `CLOUD_IAM_AUTH_ENABLED` | Whether to enable authentication against Google Cloud IAM. |
| `KEYSTORE_PATH` | The path to the keystore containing the key with which to sign JWTs. |
| `KEYSTORE_PASS` | The password of the abovementioned keystore. |
| `NEXUS_DOCKER_HOST` | The host used to access the Nexus Docker registry. |
Expand Down
10 changes: 2 additions & 8 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ plugins {
group 'com.travelaudience.nexus'
version '1.0.0'

mainClassName = 'io.vertx.core.Launcher'
mainClassName = 'com.travelaudience.nexus.proxy.Main'
sourceCompatibility = 1.8
targetCompatibility = 1.8

Expand Down Expand Up @@ -51,13 +51,7 @@ test {

shadowJar {
classifier = null

manifest {
attributes 'Main-Verticle': 'com.travelaudience.devops.nexus.proxy.NexusProxyVerticle'
}
mergeServiceFiles {
include 'META-INF/services/io.vertx.core.spi.VerticleFactory'
}
version = null
}

task wrapper(type: Wrapper) {
Expand Down
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
@@ -1 +1 @@
rootProject.name = 'nexus-google-iam-proxy'
rootProject.name = 'nexus-proxy'
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package com.travelaudience.nexus.proxy;

import static com.travelaudience.nexus.proxy.ContextKeys.PROXY;
import static com.travelaudience.nexus.proxy.Paths.ALL_PATHS;
import static com.travelaudience.nexus.proxy.Paths.ROOT_PATH;

import com.google.common.primitives.Ints;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.net.PfxOptions;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.VirtualHostHandler;

import java.util.regex.Pattern;

public abstract class BaseNexusProxyVerticle extends AbstractVerticle {
private static final String ALLOWED_USER_AGENTS_ON_ROOT_REGEX = System.getenv("ALLOWED_USER_AGENTS_ON_ROOT_REGEX");
private static final Integer BIND_PORT = Ints.tryParse(System.getenv("BIND_PORT"));
private static final String NEXUS_RUT_HEADER = System.getenv("NEXUS_RUT_HEADER");
private static final String TLS_CERT_PK12_PATH = System.getenv("TLS_CERT_PK12_PATH");
private static final String TLS_CERT_PK12_PASS = System.getenv("TLS_CERT_PK12_PASS");
private static final Boolean TLS_ENABLED = Boolean.parseBoolean(System.getenv("TLS_ENABLED"));
private static final Integer UPSTREAM_DOCKER_PORT = Ints.tryParse(System.getenv("UPSTREAM_DOCKER_PORT"));
private static final String UPSTREAM_HOST = System.getenv("UPSTREAM_HOST");
private static final Integer UPSTREAM_HTTP_PORT = Ints.tryParse(System.getenv("UPSTREAM_HTTP_PORT"));

protected final String nexusDockerHost = System.getenv("NEXUS_DOCKER_HOST");
protected final String nexusHttpHost = System.getenv("NEXUS_HTTP_HOST");

/**
* The pattern against which to match 'User-Agent' headers.
*/
private static final Pattern ALLOWED_USER_AGENTS_ON_ROOT_PATTERN = Pattern.compile(
ALLOWED_USER_AGENTS_ON_ROOT_REGEX,
Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE
);

@Override
public final void start() throws Exception {
final NexusHttpProxy dockerProxy = NexusHttpProxy.create(
vertx,
UPSTREAM_HOST,
UPSTREAM_DOCKER_PORT,
NEXUS_RUT_HEADER
);
final NexusHttpProxy httpProxy = NexusHttpProxy.create(
vertx,
UPSTREAM_HOST,
UPSTREAM_HTTP_PORT,
NEXUS_RUT_HEADER
);
final Router router = Router.router(
vertx
);

preconfigureRouting(router);

router.route(ROOT_PATH).handler(ctx -> {
final String agent = ctx.request().headers().get(HttpHeaders.USER_AGENT);

if (agent != null && ALLOWED_USER_AGENTS_ON_ROOT_PATTERN.matcher(agent).find()) {
ctx.response().setStatusCode(200).end();
} else {
ctx.next();
}
});

router.route(ALL_PATHS).handler(VirtualHostHandler.create(nexusDockerHost, ctx -> {
ctx.data().put(PROXY, dockerProxy);
ctx.next();
}));

router.route(ALL_PATHS).handler(VirtualHostHandler.create(nexusHttpHost, ctx -> {
ctx.data().put(PROXY, httpProxy);
ctx.next();
}));

configureRouting(router);

router.route(ALL_PATHS).handler(ctx -> {
((NexusHttpProxy) ctx.data().get(PROXY)).proxyUserRequest(getUserId(ctx), ctx.request(), ctx.response());
});

final PfxOptions pfxOptions = new PfxOptions().setPath(TLS_CERT_PK12_PATH).setPassword(TLS_CERT_PK12_PASS);

vertx.createHttpServer(
new HttpServerOptions().setSsl(TLS_ENABLED).setPfxKeyCertOptions(pfxOptions)
).requestHandler(
router::accept
).listen(BIND_PORT);
}

/**
* Configures the main routes. This will be called after {@link BaseNexusProxyVerticle#preconfigureRouting(Router)},
* after user-agent checking on root and after the setup of virtual hosts handlers, but before the actual proxying.
* @param router the {@link Router} which to configure.
*/
protected abstract void configureRouting(final Router router);

/**
* Returns the currently authenticated user, or {@code null} if no valid authentication info is present.
* @param ctx the current routing context.
* @return the currently authenticated user, or {@code null} if no valid authentication info is present.
*/
protected abstract String getUserId(final RoutingContext ctx);

/**
* Configures prerouting routes. This will be called right after the creation of {@code router}.
* @param router the {@link Router} which to configure.
*/
protected abstract void preconfigureRouting(final Router router);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.travelaudience.nexus.proxy;

import static com.google.api.services.cloudresourcemanager.CloudResourceManagerScopes.CLOUD_PLATFORM_READ_ONLY;
import static com.google.api.services.oauth2.Oauth2Scopes.USERINFO_EMAIL;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.api.client.auth.oauth2.Credential;
Expand All @@ -21,10 +25,6 @@
import java.util.List;
import java.util.Set;

import static com.google.api.services.cloudresourcemanager.CloudResourceManagerScopes.CLOUD_PLATFORM_READ_ONLY;
import static com.google.api.services.oauth2.Oauth2Scopes.USERINFO_EMAIL;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

/**
* Wraps {@link GoogleAuthorizationCodeFlow} caching authorization results and providing unchecked methods.
*/
Expand Down Expand Up @@ -170,8 +170,7 @@ public final Credential loadCredential(final String userId) {
* authorization code.
*
* @param authorizationCode the authorization code to use.
* @return a {@link GoogleTokenResponse} corresponding to an authorization code token request based on the given
* authorization code.
* @return a {@link GoogleTokenResponse} corresponding to an auth code token request based on the given auth code.
*/
public final GoogleTokenResponse requestToken(final String authorizationCode) {
try {
Expand Down
Loading

0 comments on commit 66b1844

Please sign in to comment.