Commit f77b9623 authored by Pierre Smeyers's avatar Pierre Smeyers
Browse files

initial commit

parents
Loading
Loading
Loading
Loading

.gitignore

0 → 100644
+34 −0
Original line number Diff line number Diff line
/target/
/bin/
/.gradle/
!.mvn/wrapper/maven-wrapper.jar
/cypress/reports/
/cypress/videos/
/cypress/screenshots/
/lsp/

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr

### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/

### VS Code ###
/.vscode/

.gitlab-ci.yml

0 → 100644
+55 −0
Original line number Diff line number Diff line
# included templates
include:
  # Go template
  - project: "to-be-continuous/golang"
    ref: "1.2.0"
    file: "templates/gitlab-ci-golang.yml"
  # Docker template
  - project: "to-be-continuous/docker"
    ref: "1.2.0"
    file: "templates/gitlab-ci-docker.yml"

# your pipeline stages
stages:
  - build
  - test
  - package-build
  - package-test
  - infra
  - deploy
  - acceptance
  - publish
  - infra-prod
  - production

# Global variables
variables:
  GO_BUILD_ARGS: install -tags netgo ./...
  DOCKER_BUILD_ARGS: "--build-arg CI_PROJECT_URL --build-arg DEFAULT_VAULT_URL"

test-service:
  image: "curlimages/curl"
  services:
    - name: "$DOCKER_SNAPSHOT_IMAGE"
      alias: "vault-secrets-provider"
  variables:
    # variables have to be explicitly declared in the YAML to be exported to the service
    VAULT_BASE_URL: "$TEST_VAULT_BASE_URL"
    VAULT_ROLE_ID: "$TEST_VAULT_ROLE_ID"
    VAULT_SECRET_ID: "$TEST_VAULT_SECRET_ID"
  stage: acceptance
  script:
    - curl -s -S -f "http://vault-secrets-provider/health"
    - |
      if [[ "$TEST_SECRET_PATH" ]]; then
        my_secret=$(curl -s -S -f "http://vault-secrets-provider/api/secrets/$TEST_SECRET_PATH?field=$TEST_SECRET_FIELD")
        echo "secret retrieved - $my_secret"
      else
        echo "no secret to test - skip"
      fi
    # TODO: test a client error
  only:
    refs:
      - branches
    variables:
      - "$TEST_VAULT_BASE_URL"

Dockerfile

0 → 100644
+28 −0
Original line number Diff line number Diff line
FROM busybox:1.31.0 AS busybox

FROM scratch

ARG CI_PROJECT_URL
ARG DEFAULT_VAULT_URL
ENV DEFAULT_VAULT_URL="${DEFAULT_VAULT_URL}"
ENV PORT=80
ENV SKIP_SSL=true


# hadolint ignore=DL3048
LABEL Name="vault-secrets-provider" \
      Version="1.0.0" \
      Maintainer="tbc-dev@googlegroups.com" \
      Description="Secrets Provider able to retrieve secrets from a Vault server (Golang-based)" \
      Url=${CI_PROJECT_URL}

COPY --from=busybox /bin/wget   /wget

COPY static       /static
COPY bin/vault_service       /vault_service

HEALTHCHECK --interval=30s --timeout=5s \
    CMD ["/wget", "-Y", "off", "-O", "-", "http://localhost/health" ]

EXPOSE  ${PORT}
CMD ["/vault_service"]

README.md

0 → 100644
+142 −0
Original line number Diff line number Diff line
# Vault Secrets Provider

This project builds a Docker image with an API able to retrieve secrets from a [Vault](https://www.vaultproject.io/) server.

It is aimed at being used in GitLab CI as a [service container](https://docs.gitlab.com/ee/ci/services/)
in order to decouple the image of your jobs and the way of retrieving secrets.

## How to use ?

### Configuring Vault for your CI/CD

Before using this service, you'll have to configure your Vault server, with:

* one or several secrets,
* one [AppRole](https://www.vaultproject.io/docs/auth/approle) with required permissions to access those secrets.

:warning: The [AppRole](https://www.vaultproject.io/docs/auth/approle) used in your CI/CD shall have a **short `token_ttl`**
(let's say 10 minutes) and a **long `secret_id_ttl`** (could be infinite).

This way:

* a new client token will be regenerated every time your CI/CD pipeline is run,
* this token will be valid only for a short period of time,
* but your secret id will remain stable for a longer period.

### API overview

The service exposes one single API to read a secret from the Vault server.

:warning: the service is smart enough to auto-detect whether the Vault server is configured to use KV Secrets Engine
version 1 (unversioned mode) or version 2 (versioned mode). Thus you don't need to worry about the `data` part
in the resource path or in the response object structure.

#### Endpoint

```curl
GET /api​/secrets​/{secret_path}
```

#### Parameters

| Name                             | description                            |
| -------------------------------- | -------------------------------------- |
| `secret_path` (_path parameter_) | this is your secret location in the Vault server |
| `field` (_query parameter_)      | parameter to access a single basic field from the secret JSON payload |

#### Example

Let's suppose your have a secret stored under `/b7ecb6ebabc231/my-backend/prod` location in Vault with JSON payload:

```json
{
  "token": "ŧ0k3N",
  "secret": "$€cr€T",
  "mysql": {
    "user": "root",
    "password": "p@s5w0rd"
  }
}
```

Then you may retrieve:

* the token calling `GET http://vault-secrets-provider/api/secrets/b7ecb6ebabc231/my-backend/prod?field=token`
* the MySql password calling `GET http://vault-secrets-provider/api/secrets/b7ecb6ebabc231/my-backend/prod?field=mysql.password`

### Required image parameters

The tool requires the following environment variables to be set (as GitLab CI secret variables):

| Name              | description                            | default value     |
| ----------------- | -------------------------------------- | ----------------- |
| `VAULT_BASE_URL`  | The Vault server base API url          | _none_ |
| `VAULT_NAMESPACE` | The Vault [Namespace](https://www.vaultproject.io/api-docs#namespaces) to retrieve secrets into | _none_ |
| `VAULT_BASE_AUTH_APPROLE_PATH`| The base [AppRole authentication](https://www.vaultproject.io/api-docs/auth/approle) API path | `/auth/approle` |
| `VAULT_BASE_AUTH_JWT_PATH`    | The base [JWT/OIDC authentication](https://www.vaultproject.io/api-docs/auth/jwt) API path | `/auth/jwt` |
| `VAULT_BASE_KV_SECRETS_PATH`  | The base [Key/Value secrets](https://www.vaultproject.io/api-docs/secret/kv/kv-v1) API path | `/secret` |
| `VAULT_ROLE_ID`   | The [AppRole](https://www.vaultproject.io/docs/auth/approle) RoleID | _none_ (required to use the [AppRole](https://www.vaultproject.io/docs/auth/approle) Auth Method) |
| `VAULT_SECRET_ID` | The [AppRole](https://www.vaultproject.io/docs/auth/approle) SecretID | _none_  (required to use the [AppRole](https://www.vaultproject.io/docs/auth/approle) Auth Method) |
| `VAULT_JWT_TOKEN` | The signed [JSON Web Token](https://en.wikipedia.org/wiki/JSON_Web_Token) to login | `$CI_JOB_JWT` |
| `VAULT_JWT_ROLE`  | Name of the role against which the login is being attempted | `default_role`  (used with the [JWT/OIDC](https://www.vaultproject.io/docs/auth/jwt) Auth Method) |

If no authentication parameter is set, the image will emit an error log at startup.

### Use in GitLab CI

Finally, the Docker image can be used in your GitLab CI files as follows:

```yaml
variables:
  # variables have to be explicitly declared in the YAML to be exported to the service
  VAULT_BASE_URL: "https://vault.secrets.acme.host/v1"
  VAULT_ROLE_ID: "$VAULT_ROLE_ID"
  VAULT_SECRET_ID: "$VAULT_SECRET_ID"

deploy-job:
  image: my-deploy-tool:latest
  services: 
    # add Vault Secrets Provider as a service
    # requires that VAULT_ROLE_ID and VAULT_SECRET_ID are declared as secret variables
    - name: $CI_REGISTRY/to-be-continuous/tools/vault-secrets-provider:master
      alias: vault-secrets-provider
  before-script:
    # retrieve some token from Vault server
    - my_token=$(curl -s -S -f "http://vault-secrets-provider/api/secrets/b7ecb6ebabc231/my-backend/prod?field=token")
    # then login
    - my-deploy-tool login --token $my_token
  script:
    # deploy (pseudo code)
    - my-deploy-tool deploy --other --args
```

Depending on what is available in your docker image, you may request the service using either `curl` or `wget`:

* `curl -s -S -f "http://vault-secrets-provider/api/secrets/b7ecb6ebabc231/my-backend/prod?field=token"`
* `wget -O - "http://vault-secrets-provider/api/secrets/b7ecb6ebabc231/my-backend/prod?field=token"`

### How to test & debug

You might want to test/debug whether you have the right secret ID, role ID, secret path, secret key or so.

Simply run the Docker image with:

```bash
docker run -i --rm -it -p 8080:80 \
    --env VAULT_BASE_URL=$VAULT_BASE_URL \
    --env VAULT_SECRET_ID=$VAULT_SECRET_ID \
    --env VAULT_ROLE_ID=$VAULT_ROLE_ID \
    registry.gitlab.com/to-be-continuous/tools/vault-secrets-provider:master
```

:warning: replace `$VAULT_BASE_URL`, `$VAULT_SECRET_ID` and `$VAULT_ROLE_ID` with yours.
If you need to override other variables, simply use the `--env` CLI option.

Then you may request the API using `curl` or `wget` as follows:

```bash
curl -sSf "http://localhost:8080/api/secrets/b7ecb6ebabc231/my-backend/prod?field=token"
wget -O - "http://vault-secrets-provider/api/secrets/b7ecb6ebabc231/my-backend/prod?field=token"
```

_et voilà_ :)
+48 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 Orange & contributors
 *
 * This program is free software; you can redistribute it and/or modify it under the terms
 *
 * of the GNU Lesser General Public License as published by the Free Software Foundation;
 * either version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth
 * Floor, Boston, MA  02110-1301, USA.
 */

package internal

import (
	"encoding/json"
	"net/http"
	"strings"
)

type healthResponse struct {
	Status string `json:"status"`
}

/**
 * healthcheck endpoint
 */
func Health(writer http.ResponseWriter, request *http.Request) {
	if strings.ToUpper(request.Method) != "GET" {
		http.Error(writer, "Method not allowed", http.StatusMethodNotAllowed)
	} else {
		body, err := json.Marshal(healthResponse{Status: "OK"})
		if err != nil {
			http.Error(writer, err.Error(), http.StatusInternalServerError)
			return
		}
		writer.Header().Set("Content-Type", "application/json")
		writer.WriteHeader(http.StatusOK)
		if _, err = writer.Write(body); err != nil {
			panic("Error while sending health status response")
		}
	}
}