├── .gitignore ├── README.md ├── dynamic-secrets-k8s ├── README.md ├── config │ ├── postgres.yml │ ├── web-policy.hcl │ └── web.yml ├── configure_vault.sh ├── images │ ├── 1_deploydb.svg │ ├── 2_createrole.svg │ ├── 3_createconnection.svg │ ├── 4_rotatecreds.svg │ ├── 5_authentication.svg │ ├── 6_policy.svg │ ├── 7_secrets.svg │ ├── install.svg │ ├── vault-db.png │ ├── vault-k8s-auth.png │ ├── vault-policy-workflow-1.png │ ├── vault-policy-workflow-1.svg │ ├── vault-policy-workflow-2.png │ ├── vault-policy-workflow-2.svg │ ├── vault-policy-workflow-3.png │ └── vault-workflow-illustration-policy.png └── setup.sh ├── static_secrets ├── README.md ├── config │ ├── web-policy.hcl │ └── web.yml ├── configure_vault.sh └── setup.sh └── transform ├── Makefile ├── README.md ├── blueprint ├── README.md ├── app.hcl ├── docs.hcl ├── docs │ ├── images │ │ ├── api.png │ │ └── card.png │ └── index.md ├── files │ ├── agent_config.hcl │ ├── db_setup.sql │ ├── setup_approle.sh │ └── setup_transform.sh ├── network.hcl ├── postgres.hcl ├── secrets │ ├── role_id │ └── secret_id └── vault.hcl ├── transform-engine-go ├── .realize.yaml ├── Dockerfile ├── bin │ └── server ├── data │ └── postgres.go ├── go.mod ├── go.sum ├── handlers │ ├── health.go │ └── payment.go ├── main.go └── vault │ └── vault.go └── transform-engine-java ├── .classpath ├── .project ├── .settings ├── org.eclipse.buildship.core.prefs └── org.eclipse.jdt.core.prefs ├── Dockerfile ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src └── main ├── java └── payments │ ├── Application.java │ ├── HealthResponse.java │ ├── PaymentRequest.java │ ├── PaymentResponse.java │ ├── PaymentsController.java │ ├── model │ └── Order.java │ ├── repository │ └── OrderRepository.java │ └── vault │ ├── TokenRequest.java │ ├── TokenResponse.java │ └── VaultClient.java └── resources └── application.yml /.gitignore: -------------------------------------------------------------------------------- 1 | ca.crt 2 | .git 3 | vscode.code-workspace 4 | transform/transform-engine-java/build 5 | transform/transform-engine-java/bin 6 | transform/transform-engine-java/.gradle 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HashiCorp Vault Demos 2 | 3 | Examples and demos for HashiCorp Vault 4 | 5 | ## [Dynamic Database Secrets with Kubernetes](./dynamic-secrets-k8s/) 6 | 7 | This demo shows how to dynamically generate PostgreSQL credentials using Vault and how to inject them into a pod using the Kubnernetes integration 8 | 9 | [https://www.vaultproject.io/docs/platform/k8s/injector/index.html](https://www.vaultproject.io/docs/platform/k8s/injector/index.html) 10 | 11 | ## [Static Secrets with Kubernetes](./static-secrets-k8s/) 12 | 13 | This demo shows how to use and inject Static Vault secrets into a Kubernetes pod 14 | 15 | [https://www.vaultproject.io/docs/platform/k8s/injector/index.html](https://www.vaultproject.io/docs/platform/k8s/injector/index.html) 16 | 17 | ## [Format Preserving Encryption with the Transform Secrets Engine](./transform/) 18 | 19 | This demo shows how to encrypt data while preserving data formatting using the Transform secrets engine 20 | 21 | [https://www.vaultproject.io/docs/secrets/transform](https://www.vaultproject.io/docs/secrets/transform) -------------------------------------------------------------------------------- /dynamic-secrets-k8s/config/postgres.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: postgres 6 | labels: 7 | app: postgres 8 | spec: 9 | type: ClusterIP 10 | ports: 11 | - port: 5432 12 | targetPort: 5432 13 | selector: 14 | app: postgres 15 | 16 | --- 17 | apiVersion: apps/v1 18 | kind: Deployment 19 | metadata: 20 | name: postgres 21 | spec: 22 | replicas: 1 23 | selector: 24 | matchLabels: 25 | service: postgres 26 | app: postgres 27 | template: 28 | metadata: 29 | labels: 30 | service: postgres 31 | app: postgres 32 | spec: 33 | containers: 34 | - name: postgres 35 | image: hashicorpdemoapp/postgres:11.6 36 | ports: 37 | - containerPort: 5432 38 | env: 39 | - name: POSTGRES_DB 40 | value: wizard 41 | - name: POSTGRES_USER 42 | value: postgres 43 | - name: POSTGRES_PASSWORD 44 | value: password 45 | volumeMounts: 46 | - mountPath: "/var/lib/postgresql/data" 47 | name: "pgdata" 48 | volumes: 49 | - name: pgdata 50 | emptyDir: {} 51 | -------------------------------------------------------------------------------- /dynamic-secrets-k8s/config/web-policy.hcl: -------------------------------------------------------------------------------- 1 | path "database/creds/db-app" { 2 | capabilities = ["read"] 3 | } -------------------------------------------------------------------------------- /dynamic-secrets-k8s/config/web.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Service to expose web frontend 3 | 4 | apiVersion: v1 5 | kind: Service 6 | metadata: 7 | name: web-service 8 | spec: 9 | selector: 10 | app: web 11 | ports: 12 | - name: http 13 | protocol: TCP 14 | port: 9090 15 | targetPort: 9090 16 | 17 | --- 18 | # Service account to allow pod access to Vault via K8s auth 19 | 20 | apiVersion: v1 21 | kind: ServiceAccount 22 | metadata: 23 | name: web 24 | automountServiceAccountToken: true 25 | 26 | --- 27 | # Web frontend 28 | 29 | apiVersion: apps/v1 30 | kind: Deployment 31 | metadata: 32 | name: web-deployment 33 | labels: 34 | app: web 35 | spec: 36 | replicas: 2 37 | selector: 38 | matchLabels: 39 | app: web 40 | template: 41 | metadata: 42 | labels: 43 | app: web 44 | annotations: 45 | vault.hashicorp.com/agent-inject: "true" 46 | vault.hashicorp.com/agent-inject-secret-db-creds: "database/creds/db-app" 47 | vault.hashicorp.com/agent-inject-template-db-creds: | 48 | { 49 | {{ with secret "database/creds/db-app" -}} 50 | "db_connection": "host=postgres port=5432 user={{ .Data.username }} password={{ .Data.password }} dbname=wizard sslmode=disable" 51 | {{- end }} 52 | } 53 | vault.hashicorp.com/role: "web" 54 | spec: 55 | serviceAccountName: web 56 | containers: 57 | - name: web 58 | image: hashicorpdemoapp/product-api:v0.0.4 59 | ports: 60 | - containerPort: 9090 61 | env: 62 | - name: "CONFIG_FILE" 63 | value: "/vault/secrets/db-creds" 64 | -------------------------------------------------------------------------------- /dynamic-secrets-k8s/configure_vault.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Enable and configure Kubernetes Authentication 4 | vault auth enable kubernetes 5 | 6 | kubectl exec $(kubectl get pods --selector "app.kubernetes.io/instance=vault,component=server" -o jsonpath="{.items[0].metadata.name}") -c vault -- \ 7 | sh -c ' \ 8 | vault write auth/kubernetes/config \ 9 | token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \ 10 | kubernetes_host=https://${KUBERNETES_PORT_443_TCP_ADDR}:443 \ 11 | kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt' 12 | 13 | # Enable and configure PostgresSQL Dynamic secrets 14 | vault secrets enable database 15 | 16 | vault write database/config/wizard \ 17 | plugin_name=postgresql-database-plugin \ 18 | verify_connection=false \ 19 | allowed_roles="*" \ 20 | connection_url="postgresql://{{username}}:{{password}}@postgres:5432/wizard?sslmode=disable" \ 21 | username="postgres" \ 22 | password="password" 23 | 24 | # Rotate the database root password 25 | vault write --force database/rotate-root/wizard 26 | 27 | # Create a role allowing credentials to be created with access for all tables in the DB 28 | vault write database/roles/db-app \ 29 | db_name=wizard \ 30 | creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \ 31 | GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \ 32 | revocation_statements="ALTER ROLE \"{{name}}\" NOLOGIN;"\ 33 | default_ttl="1h" \ 34 | max_ttl="24h" 35 | 36 | # Write the policy to allow read access to the role 37 | vault policy write web-dynamic ./config/web-policy.hcl 38 | 39 | # Assign the policy to users who authenticate with Kubernetes service accounts called web 40 | vault write auth/kubernetes/role/web \ 41 | bound_service_account_names=web \ 42 | bound_service_account_namespaces=default \ 43 | policies=web-dynamic \ 44 | ttl=1h 45 | -------------------------------------------------------------------------------- /dynamic-secrets-k8s/images/3_createconnection.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 62 | 85 | 86 | 87 | 88 | 89 | 90 | demo-vault-kubernetes/dynamic-secrets-k8s on  master [✘!?] via 🐹 v1.13.1 at ☸ default c cl cle clea clear clear clear vault write database/config/wizard \ plugin_name=postgresql-database-plugin \ allowed_roles="*" \ connection_url="postgresql://{{username}}:{{password}}@postgres:5432/wizard?sslmode=disable" \ username="postgres" \ password="password" vault write database/config/wizard \ plugin_name=postgresql-database-plugin \ allowed_roles="*" \ connection_url="postgresql://{{username}}:{{password}}@postgres:5432/wizard?sslmode=disable" \ password="password" connection_url="postgresql://{{username}}:{{password}}@postgres:5432/wizard?sslmode=disable" \ username="postgres" \ password="password" password="password" password="password" 91 | -------------------------------------------------------------------------------- /dynamic-secrets-k8s/images/4_rotatecreds.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 76 | 99 | 100 | 101 | 102 | 103 | 104 | demo-vault-kubernetes/dynamic-secrets-k8s on  master [✘!?] via 🐹 v1.13.1 at ☸ default c cl cle clea clear clear clear vault write --force /database/rotate-root/wizard vault write --force /database/rotate-root/wizard vault write --force /database/rotate-root/wizard Success! Data written to: database/rotate-root/wizardkubectl exec -it $(kubectl get pods --selector "app=postgres" -o jsonpath="{.items[0].metadata.name}") -c postgres -- bash -c 'PGPASSWORD=password psql -U postgres' kubectl exec -it $(kubectl get pods --selector "app=postgres" -o jsonpath="{.items[0].metadata.name}") -c postgres -- bash -c 'PGPASSWORD=password psql -U postgres' ") -c postgres -- bash -c 'PGPASSWORD=password psql -U postgres' psql: FATAL: password authentication failed for user "postgres"command terminated with exit code 2 vault read database/creds/db-app vault read database/creds/db-app vault read database/creds/db-app Key Value--- -----lease_id database/creds/db-app/CpglqjGSr6CW5VOripx78rUvlease_duration 1hlease_renewable truepassword A1a-CluAj9zv6fr2zA3Susername v-token-db-app-y72kjuPuv4Gp4XP74zZd-1576779550 105 | -------------------------------------------------------------------------------- /dynamic-secrets-k8s/images/5_authentication.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 73 | 96 | 97 | 98 | 99 | 100 | 101 | demo-vault-kubernetes/dynamic-secrets-k8s on  master [✘!?] via 🐹 v1.13.1 at ☸ default c cl cle clea clear clear clear vault auth enable kubernetes vault auth enable kubernetes vault auth enable kubernetes Success! Enabled kubernetes auth method at: kubernetes/kubectl exec $(kubectl get pods --selector "app.kubernetes.io/instance=vault,component=server" -o jsonpath="{.items[0].metadata.name}") -c vault -- \ sh -c ' \ vault write auth/kubernetes/config \ token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \ kubernetes_host=https://${KUBERNETES_PORT_443_TCP_ADDR}:443 \ kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt' kubectl exec $(kubectl get pods --selector "app.kubernetes.io/instance=vault,component=server" -o jsonpath="{.items[0].metadata.name}") -c vault -- \ sh -c ' \ vault write auth/kubernetes/config \ kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt' vault write auth/kubernetes/config \ token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \ kubernetes_host=https://${KUBERNETES_PORT_443_TCP_ADDR}:443 \ kubernetes_host=https://${KUBERNETES_PORT_443_TCP_ADDR}:443 \ kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt' kubectl exec $(kubectl get pods --selector "app.kubernetes.io/instance=vault,component=server" -o jsonpath="{.items[0].metadata.name}") -c vault -- \ sh -c ' \ vault write auth/kubernetes/config \ token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \ kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt' token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \ kubernetes_host=https://${KUBERNETES_PORT_443_TCP_ADDR}:443 \ kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt' kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt' Success! Data written to: auth/kubernetes/config 102 | -------------------------------------------------------------------------------- /dynamic-secrets-k8s/images/6_policy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 68 | 91 | 92 | 93 | 94 | 95 | 96 | demo-vault-kubernetes/dynamic-secrets-k8s on  master [✘!?] via 🐹 v1.13.1 at ☸ default c cl cle clea clear clear vault policy write web ./config/web-policy.hcl vault policy write web ./config/web-policy.hcl vault policy write web ./config/web-policy.hcl Success! Uploaded policy: webvault write auth/kubernetes/role/web \ bound_service_account_names=web \ bound_service_account_namespaces=default \ policies=web \ ttl=1h vault write auth/kubernetes/role/web \ bound_service_account_names=web \ bound_service_account_namespaces=default \ policies=web \ ttl=1h ttl=1h Success! Data written to: auth/kubernetes/role/web 97 | -------------------------------------------------------------------------------- /dynamic-secrets-k8s/images/vault-db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholasjackson/demo-vault/0e6bd10b886fb72b83f017895dfa4d2c9eac120c/dynamic-secrets-k8s/images/vault-db.png -------------------------------------------------------------------------------- /dynamic-secrets-k8s/images/vault-k8s-auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholasjackson/demo-vault/0e6bd10b886fb72b83f017895dfa4d2c9eac120c/dynamic-secrets-k8s/images/vault-k8s-auth.png -------------------------------------------------------------------------------- /dynamic-secrets-k8s/images/vault-policy-workflow-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholasjackson/demo-vault/0e6bd10b886fb72b83f017895dfa4d2c9eac120c/dynamic-secrets-k8s/images/vault-policy-workflow-1.png -------------------------------------------------------------------------------- /dynamic-secrets-k8s/images/vault-policy-workflow-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholasjackson/demo-vault/0e6bd10b886fb72b83f017895dfa4d2c9eac120c/dynamic-secrets-k8s/images/vault-policy-workflow-2.png -------------------------------------------------------------------------------- /dynamic-secrets-k8s/images/vault-policy-workflow-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /dynamic-secrets-k8s/images/vault-policy-workflow-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholasjackson/demo-vault/0e6bd10b886fb72b83f017895dfa4d2c9eac120c/dynamic-secrets-k8s/images/vault-policy-workflow-3.png -------------------------------------------------------------------------------- /dynamic-secrets-k8s/images/vault-workflow-illustration-policy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholasjackson/demo-vault/0e6bd10b886fb72b83f017895dfa4d2c9eac120c/dynamic-secrets-k8s/images/vault-workflow-illustration-policy.png -------------------------------------------------------------------------------- /dynamic-secrets-k8s/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | echo "##################################################" 3 | echo "Install shipyard if you do not have it installed" 4 | echo "curl https://shipyard.demo.gs/install.sh | bash" 5 | echo "##################################################" 6 | 7 | yard up --enable-consul false -------------------------------------------------------------------------------- /static_secrets/README.md: -------------------------------------------------------------------------------- 1 | # Static Secrets with Vault and Kubernetes 2 | 3 | README coming soon, 4 | 5 | ## Enable and configure Kubernetes Authentication 6 | 7 | ```shell 8 | vault auth enable kubernetes 9 | 10 | kubectl exec $(kubectl get pods --selector "app.kubernetes.io/instance=vault,component=server" -o jsonpath="{.items[0].metadata.name}") -c vault -- \ 11 | sh -c ' \ 12 | vault write auth/kubernetes/config \ 13 | token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \ 14 | kubernetes_host=https://${KUBERNETES_PORT_443_TCP_ADDR}:443 \ 15 | kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt' 16 | ``` 17 | 18 | ## Write a secret to the Vault 19 | 20 | ```shell 21 | vault kv put secret/web api_key=abc123fd 22 | ``` 23 | 24 | ## Write policy allowing read access to that secret 25 | 26 | ```shell 27 | vault policy write web-static ./config/web-policy.hcl 28 | ``` 29 | 30 | ## Assign the policy to users who authenticate with Kubernetes service accounts called web 31 | 32 | ```shell 33 | vault write auth/kubernetes/role/web \ 34 | bound_service_account_names=web \ 35 | bound_service_account_namespaces=default \ 36 | policies=web-static \ 37 | ttl=1h 38 | ``` 39 | 40 | ## Apply the Config with annotations to inject secrets 41 | 42 | ```yaml 43 | annotations: 44 | vault.hashicorp.com/agent-inject: "true" 45 | vault.hashicorp.com/agent-inject-secret-web: secret/data/web 46 | vault.hashicorp.com/agent-inject-template-web: | 47 | { 48 | {{ with secret "secret/data/web" -}} 49 | "api_key": "{{ .Data.data.api_key }}" 50 | {{- end }} 51 | } 52 | vault.hashicorp.com/role: "web" 53 | ``` 54 | 55 | ## Check the result 56 | 57 | ```shell 58 | kubectl exec -it $(kubectl get pods --selector "app=web" -o jsonpath="{.items[0].metadata.name}") -c web -- cat /vault/secrets/web 59 | ``` -------------------------------------------------------------------------------- /static_secrets/config/web-policy.hcl: -------------------------------------------------------------------------------- 1 | path "secret/data/web" { 2 | capabilities = ["read"] 3 | } -------------------------------------------------------------------------------- /static_secrets/config/web.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Service to expose web frontend 3 | 4 | apiVersion: v1 5 | kind: Service 6 | metadata: 7 | name: web-service 8 | spec: 9 | selector: 10 | app: web 11 | ports: 12 | - name: http 13 | protocol: TCP 14 | port: 9090 15 | targetPort: 9090 16 | 17 | --- 18 | # Service account to allow pod access to Vault via K8s auth 19 | 20 | apiVersion: v1 21 | kind: ServiceAccount 22 | metadata: 23 | name: web 24 | automountServiceAccountToken: true 25 | 26 | --- 27 | # Web frontend 28 | 29 | apiVersion: apps/v1 30 | kind: Deployment 31 | metadata: 32 | name: web-deployment 33 | labels: 34 | app: web 35 | spec: 36 | replicas: 2 37 | selector: 38 | matchLabels: 39 | app: web 40 | template: 41 | metadata: 42 | labels: 43 | app: web 44 | annotations: 45 | vault.hashicorp.com/agent-inject: "true" 46 | vault.hashicorp.com/agent-inject-secret-web: secret/data/web 47 | vault.hashicorp.com/agent-inject-template-web: | 48 | { 49 | {{ with secret "secret/data/web" -}} 50 | "api_key": "{{ .Data.data.api_key }}" 51 | {{- end }} 52 | } 53 | vault.hashicorp.com/role: "web" 54 | spec: 55 | serviceAccountName: web 56 | containers: 57 | - name: web 58 | image: hashicorpdemoapp/product-api:v0.0.4 59 | ports: 60 | - containerPort: 9090 61 | env: 62 | - name: "CONFIG_FILE" 63 | value: "/vault/secrets/web" 64 | -------------------------------------------------------------------------------- /static_secrets/configure_vault.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Enable and configure Kubernetes Authentication 4 | vault auth enable kubernetes 5 | 6 | kubectl exec $(kubectl get pods --selector "app.kubernetes.io/instance=vault,component=server" -o jsonpath="{.items[0].metadata.name}") -c vault -- \ 7 | sh -c ' \ 8 | vault write auth/kubernetes/config \ 9 | token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \ 10 | kubernetes_host=https://${KUBERNETES_PORT_443_TCP_ADDR}:443 \ 11 | kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt' 12 | 13 | # Write a secret to the database 14 | vault kv put secret/web api_key=abc123fd 15 | 16 | # Write policy allowing read access to that secret 17 | vault policy write web-static ./config/web-policy.hcl 18 | 19 | # Assign the policy to users who authenticate with Kubernetes service accounts called web 20 | vault write auth/kubernetes/role/web \ 21 | bound_service_account_names=web \ 22 | bound_service_account_namespaces=default \ 23 | policies=web-static \ 24 | ttl=1h -------------------------------------------------------------------------------- /static_secrets/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | echo "##################################################" 3 | echo "Install shipyard if you do not have it installed" 4 | echo "curl https://shipyard.demo.gs/install.sh | bash" 5 | echo "##################################################" 6 | 7 | yard up --enable-consul false -------------------------------------------------------------------------------- /transform/Makefile: -------------------------------------------------------------------------------- 1 | image_name="nicholasjackson/transform-demo" 2 | 3 | build_docker_go: 4 | docker build -t ${image_name}:go -f transform-engine-go/Dockerfile ./transform-engine-go 5 | 6 | build_docker_java: 7 | docker build -t ${image_name}:java -f transform-engine-java/Dockerfile ./transform-engine-java 8 | 9 | build_docker_all: build_docker_go build_docker_java 10 | 11 | push_all: build_docker_all 12 | docker push ${image_name}:go 13 | docker push ${image_name}:java -------------------------------------------------------------------------------- /transform/README.md: -------------------------------------------------------------------------------- 1 | # Format preserving Encryption with the Vault Transform Secrets Engine 2 | This demo shows how to use the Transform secrets engine to encrypt data while preserving data formatting. 3 | 4 | Note: This demo uses Vault Enterprise 1.4, Vault Enterprise in trial mode is limited to sessions of 30 minutes before the Vault server will enter sealed state. You will need to restart the server to reactivate the trial. 5 | 6 | ## Requirements: 7 | * Docker []() 8 | * Shipyard [https://shipyard.run/docs/install](https://shipyard.run/docs/install) 9 | 10 | ## Running the demo 11 | The demo uses Shipyard to start a Vault server and example application in Docker on your local machine. To run the demo use the following command: 12 | 13 | ```shell 14 | ➜ shipyard run ./blueprint 15 | Running configuration from: ./blueprint 16 | 17 | 2020-06-02T07:10:00.190+0100 [DEBUG] Statefile does not exist 18 | 2020-06-02T07:10:00.191+0100 [INFO] Creating Network: ref=local 19 | ``` 20 | 21 | Once started interactive documentation can be run for the demo at [http://docs.docs.shipyard.run:8080/docs/index](http://docs.docs.shipyard.run:8080/docs/index). The Vault server, Postgres server and example applications are also accessible from your terminal at the following locations: 22 | 23 | * Vault `localhost:8200` 24 | * PostgreSQL `localhost:5432` DB: `payments`, User: `root`, Pass: `password` 25 | * Java example application `localhost:9092` 26 | * Go example application `localhost:9091` 27 | 28 | ## Stopping the demo 29 | To stop the demo and clean up resources, run the following command: 30 | 31 | ```shell 32 | demo-vault/transform on  master [!] via 🐹 v1.13.8 on 🐳 v19.03.10 () 33 | ➜ shipyard destroy 34 | 2020-06-02T07:08:14.044+0100 [INFO] Destroy Container: ref=vault 35 | 2020-06-02T07:08:14.045+0100 [INFO] Destroy Container: ref=payments_go 36 | 2020-06-02T07:08:14.045+0100 [INFO] Destroy Container: ref=payments_java 37 | 2020-06-02T07:08:14.045+0100 [INFO] Destroy Container: ref=postgres 38 | 2020-06-02T07:08:14.045+0100 [INFO] Destroy Documentation: ref=docs 39 | 2020-06-02T07:08:15.506+0100 [INFO] Destroy Network: ref=local 40 | ``` 41 | 42 | ## Source Code 43 | Source code for the example applications can be found in this repository at the following locations: 44 | * Go [./transform-engine-go](./transform-engine-go) 45 | * Java [./transform-engine-java](./transform-engine-java) -------------------------------------------------------------------------------- /transform/blueprint/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Vault Transform Example 3 | author: Nic Jackson 4 | slug: vault_transform 5 | env: 6 | - VAULT_ADDR=http://localhost:8200 7 | - VAULT_TOKEN=root 8 | --- 9 | 10 | # Environment Variables 11 | To interact with the Vault server using the Vault CLI, set the following environment variables: 12 | 13 | ```shell 14 | export VAULT_ADDR="http://localhost:8200" 15 | export VAULT_TOKEN="root" 16 | ``` 17 | 18 | # Vault Server Shell 19 | If you do not have the Vault CLI installed, you can use Shipyard to create an interactive shell 20 | on the Vault server. 21 | 22 | ```shell 23 | shipyard exec container.vault 24 | ``` 25 | 26 | # Using the Transform Secrets engine 27 | 28 | After the Vault container starts the `transform` is automatically 29 | configured using the script `./files/setup_vault.sh`. This sets up the 30 | role and transform values. 31 | 32 | To write values using the `transform` engine: 33 | 34 | ```shell 35 | vault write transform/encode/payments value=1111-2222-3333-4444 36 | 37 | Key Value 38 | 39 | encoded_value 9300-3376-4943-8903 40 | ``` 41 | 42 | To read values using the `transform` secrets: 43 | 44 | ```shell 45 | vault write transform/decode/payments value=9300-3376-4943-8903 46 | Key Value 47 | 48 | decoded_value 1111-2222-3333-4444 49 | ``` -------------------------------------------------------------------------------- /transform/blueprint/app.hcl: -------------------------------------------------------------------------------- 1 | container "payments_go" { 2 | image { 3 | name = "nicholasjackson/transform-demo:go" 4 | } 5 | 6 | command = ["/app/server"] 7 | 8 | port { 9 | local = 9090 10 | remote = 9090 11 | host = 9091 12 | } 13 | 14 | env { 15 | key = "POSTGRES_HOST" 16 | value = "postgres.container.shipyard.run" 17 | } 18 | 19 | env { 20 | key = "POSTGRES_PORT" 21 | value = "5432" 22 | } 23 | 24 | env { 25 | key = "VAULT_TOKEN" 26 | value = "root" 27 | } 28 | 29 | env { 30 | key = "VAULT_ADDR" 31 | value = "http://vault.container.shipyard.run:8200" 32 | } 33 | 34 | network { 35 | name = "network.local" 36 | } 37 | } 38 | 39 | container "payments_java" { 40 | image { 41 | name = "nicholasjackson/transform-demo:java" 42 | } 43 | 44 | command = [ 45 | "java", 46 | "-jar", 47 | "/app/spring-boot-payments-0.1.0.jar", 48 | ] 49 | 50 | port { 51 | local = 9090 52 | remote = 9090 53 | host = 9092 54 | } 55 | 56 | env { 57 | key = "spring_datasource_url" 58 | value = "jdbc:postgresql://postgres.container.shipyard.run:5432/payments" 59 | } 60 | 61 | env { 62 | key = "vault_token" 63 | value = "root" 64 | } 65 | 66 | env { 67 | key = "vault_addr" 68 | value = "http://vault.container.shipyard.run:8200" 69 | } 70 | 71 | network { 72 | name = "network.local" 73 | } 74 | } -------------------------------------------------------------------------------- /transform/blueprint/docs.hcl: -------------------------------------------------------------------------------- 1 | docs "docs" { 2 | path = "./docs" 3 | port = 8080 4 | open_in_browser = true 5 | 6 | network { 7 | name = "network.local" 8 | } 9 | 10 | index_title = "Transform" 11 | index_pages = ["index"] 12 | } -------------------------------------------------------------------------------- /transform/blueprint/docs/images/api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholasjackson/demo-vault/0e6bd10b886fb72b83f017895dfa4d2c9eac120c/transform/blueprint/docs/images/api.png -------------------------------------------------------------------------------- /transform/blueprint/docs/images/card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholasjackson/demo-vault/0e6bd10b886fb72b83f017895dfa4d2c9eac120c/transform/blueprint/docs/images/card.png -------------------------------------------------------------------------------- /transform/blueprint/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: index 3 | title: Encrypting Data while Preserving Formatting with the Vault Enterprise Transform Secrets Engine 4 | sidebar_label: Introduction 5 | --- 6 | 7 | import Tabs from '@theme/Tabs'; 8 | import TabItem from '@theme/TabItem'; 9 | 10 | Vault 1.4 Enterprise introduced a new secrets engine called Transform. Transform is a secrets engine that allows Vault to encode and decode sensitive values residing in external systems such as databases or file systems. The Transform engine allows you to ensure that when a system is compromised, and its data is leaked, that the encoded secrets remain uncompromised even when held by an adversary. Unlike the Transit secrets engine, with Transform you can encrypt data while preserving the original formatting. 11 | 12 | This post shows you how to implement Transform secrets into a simple API; source code is provided for both the Java and Go programming languages. 13 | 14 | For information on the technical detail behind the Transform engine please see Andy's excelent article [https://www.hashicorp.com/blog/transform-secrets-engine](https://www.hashicorp.com/blog/transform-secrets-engine). 15 | 16 | 17 | ### Video demonstration 18 | 19 | 20 | 21 | 22 | ## API Structure 23 | 24 | ![](./images/api.png) 25 | 26 | Our example application is a simple RESTful payment service, backed by a PostgreSQL database, there is a single route which accepts a POST request. 27 | 28 | ``` 29 | POST /order 30 | Content-Type: application/json 31 | ``` 32 | 33 | ### Request 34 | The data for the API is sent as JSON and has three fields for the `card number`, the `expiration` and the `cv2`. 35 | 36 | ```json 37 | { 38 | "card_number": "1234-1234-1234-1234", 39 | "expiration": "10/12", 40 | "cv2": "123" 41 | } 42 | ``` 43 | 44 | ### Response 45 | 46 | On a succesfull call to the API, the data is saved to the database and a transaction ID is returned to the caller. 47 | 48 | ```json 49 | { 50 | "transaction_id": "1234" 51 | } 52 | ``` 53 | 54 | ## Security Requirements 55 | The security requirements for the API are: 56 | 57 | * Credit card details must be stored in the database in an encrypted format at rest 58 | * It must be possible to infer the bank and type of a card number without decrypting it 59 | 60 | The first requirement is relatively trivial to solve using Vault and the Transit Secrets Engine. Transit secrets can be used as encryption as service to encrypt the credit card details before they are written to the database. 61 | 62 | To satisfy the second requirement, you need to be able to query the type of the card and the bank which issued it. You can get this data from the card number as not all the data in a credit card number is unique. A credit card number is composed of three parts: the Issuer Number, the Account Number and the Checksum. 63 | 64 | ![](./images/card.png) 65 | 66 | `Issuer Number` relates to the type of the card (first digit), and the issuers code this is the information which you would like to query to satisfy the second requirement. 67 | 68 | `Account Number` is the unique identifier assigned to the holder of the card 69 | 70 | `Checksum` is not a secret part of the card number instead it is desined for quick error checking. The checksum is generated from the card number, before processing the checksum is regenerated using the Luhn algorithm if the given and the computed checksums differ then the card number has been entered incorrectly. 71 | 72 | To be able to query the card issuer you realistically have two options: 73 | 74 | 1. Partially encrypt the card number in the databse 75 | 1. Store metadata for the card along with the encrypted values 76 | 77 | To implement this requirement in code, the developers have the responsiblity for managing the complexity of partially encrypting the credit card data, and information security need to worry about the correct implementation of this. 78 | 79 | ## Transform Secrets Engine 80 | Vault's Transform Secrets Engine can be used to simplify the process while still satisfying the second requirement. Transform allows you to encrypt data while preserving formatting or to partially encrypt data based on a user-configurable formula. The benefits of this are that the info security team can centrally manage the definition for the encryption process, and the developers don’t need to worry about the implementation, they can use the Transform API to encrypt the card numbers. 81 | 82 | In our use case where there is a need to partially encrypt the credit card numbers leaving the issuer as queryable data, a transform could be defined. This Transform takes a card number and encrypts the sensitive parts while retaining the formatting and ability to infer information about the card type and issuing bank. 83 | 84 | Transforms are defined as regular expressions; the capture groups inside the expression are replaced with ciphertext, and anything outside the match groups is left in the original format. 85 | 86 | To encrypt only the account number and checksum for a credit card number, you could use the following regular expression. 87 | 88 | ``` 89 | \d{4}-\d{2}(\d{2})-(\d{4})-(\d{4}) 90 | ``` 91 | 92 | Given an input credit card number: 93 | 94 | ``` 95 | 1234-5611-1111-1111 96 | ``` 97 | 98 | Vault would return the cyphertext: 99 | 100 | ``` 101 | 1234-5672-6649-0472 102 | ``` 103 | 104 | Note the first 6 digits have not been replaced with cyphertext as there are no capture groups in the regular expression for this text, the formatting of the data is also preserved as this was outside the capture groups. 105 | 106 | 107 | ## Real world impact of partially encrypting data 108 | 109 | You may be wondering, by only encrypting the account number and cv2 data, are you reducing the security of the encrypted card number? 110 | 111 | The short answer is yes, but in real terms, it probably does not make a difference. 112 | 113 | A number containing 16 digits has a possibility of 16^16 combinations, including the CV2 number. This roughly equates to 10 quintillion different permutations. 114 | 115 | If you only store 10 digits of the card number plus the CV2, this is 10^13, or about 10 trillion combinations. 116 | 117 | In reality, since the first 6 digits of a card number are the issuer and card type, there are not 1 million different issuers. Let's say there are 10,000, storing the full 16 digits would give you roughly 100 quadrillion combinations. In both cases, we need to remove the checksum, so we get 10 quadrillion combinations if you encrypt the account number and 1 trillion if you do not. 118 | 119 | Yes, not encrypting the issuer means someone can make fewer guesses to determine the number, but they still need to make 1 trillion guesses. Assuming someone managed to obtain your database containing partially encrypted card numbers. If you had an average API request time of 100ms to accept or reject a payment, it would take about 190258 years for someone to brute force a payment. Even if the attacker was running parallel attacks, the odds are stacked heavily against them. 120 | 121 | Fun math to one side, since we have determined it is secure to encrypt these credit card numbers partially, let's see how to do it. 122 | 123 | 124 | ## Configuring Transform Secrets 125 | 126 | The Transform secrets engine is only available with **Vault Enterprise version 1.4** and above. With all versions of Vault, only the the Key/Value engine and the Cubbyhole secrets engines are enabled by default. To use the Transform Secrets Engine it first needs to be enabled, you can use the following command. 127 | 128 | ```shell 129 | vault secrets enable transform 130 | ``` 131 | 132 | 133 |

134 | 135 | To encrypt data with the transform secrets engine, there are several resources which encapsulating different aspects of the transform process that need to be configured. These are: 136 | 137 | * **Roles** - Roles are the basic high-level construct that holds the set of transformation that it is allowed to performed. The role name is provided when performing encode and decode operations. 138 | 139 | * **Transformations** - Transformations hold information about a particular transformation. It contains information about the type of transformation that we want to perform, the template that it should use for value detection, and other transformation-specific values such as the tweak source or the masking character to use. 140 | 141 | * **Templates** - Templates allow us to determine what and how to capture the value that we want to transform. 142 | 143 | * **Alpahbets** - Alphabets provide the set of valid UTF-8 character contained within both the input and transformed value on FPE transformations. 144 | 145 | Let's walk through each of the steps. 146 | 147 | ### Roles 148 | 149 | First, we need to create a role called payments, when creating the role you provide the list of transformations that can be used from this role using the transformations parameter. The transformation ccn-fpe in the example role below does not yet exist. The transformations parameter is a `"soft"` constraint, while a role requires transforms to encode and decode data, they do not need to exist when creating the role. 150 | 151 | ```shell 152 | vault write transform/role/payments transformations=ccn-fpe 153 | ``` 154 | 155 | 156 |

157 | 158 | ### Transformations 159 | 160 | Next we create a transformation called `ccn-fpe`, this is the same name that was referenced when you create the role in the previous step. The parameter `type` defines the transform operation you would like to perform, this has two possible values: 161 | 162 | * `fpe` - use Format Preserving Encryption using the FF3-1 algoryrthm 163 | * `masking` - this process replaces the sensitive characters in a transformation with a desired character but is not reversable. 164 | 165 | `tweak_source` is a non-confidential value which is stored alongside the ciphertext used when performing encryption and decryption operations. This parameter takes one of three permissible values, `supplied`, `generated`, `internal`. This example uses the `internal` value which delegates the creation and storage of the tweak value to Vault. More information on tweak source can be found in the Vault documentation, [https://www.vaultproject.io/docs/secrets/transform#tweak-source](https://www.vaultproject.io/docs/secrets/transform#tweak-source). 166 | 167 | The `template` parameter relates to the template which will be used by the transform, you can define templates and reuse them across multiple different transformations. Vault has two built in Templates which can be used `builtin/creditcardnumber` and `builtin/socialsecuritynumber`. 168 | 169 | Finally, you specify the `allowed_roles`, specifying allowed roles ensures that the creator of the role is allowed to use this transformation. 170 | 171 | ``` 172 | vault write transform/transformation/ccn-fpe \ 173 | type=fpe \ 174 | tweak_source=internal \ 175 | template=ccn \ 176 | allowed_roles=payments 177 | ``` 178 | 179 | 180 |

181 | 182 | ### Templates 183 | 184 | The template defines what in the data is encrypted by the transform, templates are specfied as regular expressions, the capture groups in the expression define 185 | the elements of the input which will be replaced with cyphertext. 186 | 187 | The below example creates a template called `ccn`. The `type` parameter is set to `regex` which is currently the only option supported by the backend. Then you specify a `pattern` as a valid regular expression. For `fpe` transformations you need to specify the `alphabet`, the `alphabet` is a custom character set which will be used in the outputed cyphertext. `alphabet` can either be a custom alphabet like the example below or one of the [built in](https://www.vaultproject.io/docs/secrets/transform#alphabets) values. 188 | 189 | ```shell 190 | vault write transform/template/ccn \ 191 | type=regex \ 192 | pattern='\d{4}-\d{2}(\d{2})-(\d{4})-(\d{4})' \ 193 | alphabet=numerics 194 | ``` 195 | 196 | 197 | 198 | ### Alphabets 199 | 200 | Creating custom alphabets is an optional step for a transform, Vault has a number of built in alphabets covering common usecases however you wish your cypher text to be composed of a specific set of unicode characters. To define a custom alphabet you use the following command, this command creates a custom alphabet called `numerics` using the characters `0-9`. 201 | 202 | ```shell 203 | vault write transform/alphabet/numerics \ 204 | alphabet="0123456789" 205 | ``` 206 | 207 | 208 |

209 | 210 | ## Testing the transform 211 | 212 | Now all of the components have been configured you can test the setup by writing data to the path `transform/encode/payments`, the part of the path `payments` refers to the name of your transform created in the previous steps. 213 | 214 | ``` 215 | vault write transform/encode/payments value=1111-2222-3333-4444 216 | ``` 217 | 218 | 219 | 220 | You will see an output which looks similar to the following. Note that the first 6 digits of the returne cyphertext are the same as the original data. 221 | 222 | ``` 223 | Key Value 224 | --- ----- 225 | encoded_value 1111-2200-1452-4879 226 | ``` 227 | 228 | To decode this cyphertext and reverse the operation, you write data to the `transform/decode/payments` path. 229 | 230 | ``` 231 | vault write transform/decode/payments value= 232 | 233 | vault write transform/decode/payments value=1111-2200-1452-4879 234 | ``` 235 | 236 | You will see output which looks similar to the below example: 237 | 238 | ```shell 239 | Key Value 240 | --- ----- 241 | decoded_value 1111-2222-3333-4444 242 | ``` 243 | 244 |

245 | 246 |

247 | 248 | 249 | ## Using Transform in your application 250 | 251 | So far you have seen how you can use the Transform engine using the CLI, to use the transform engine from your application you need to use Vault's API, everything possible using the CLI is also possible using the RESTful API. To interact with the Vault API you have three options: 252 | 253 | 1. Use one of the [Client libraries](https://www.vaultproject.io/api/libraries.html) 254 | 1. Code generate your own client using the [OpenAPI v3 specifications](https://www.vaultproject.io/api-docs/system/internal-specs-openapi) 255 | 1. Manually interact with the HTTP API 256 | 257 | This example is going to demonstrate the third option, as this demonstrates the simplicity with interacting with Vault's API. 258 | 259 | The example application only needs to encode data, and not manage the configuration for Transform, to do this it only needs to interact with a single API endpoint which is Encode. 260 | 261 | https://www.vaultproject.io/api-docs/secret/transform#encode 262 | 263 | 264 | ## Using the Transform Encode API 265 | 266 | The application only needs to encode data and not manage the configuration for Transform; to do this, it only needs to interact with a single API endpoint, which is Encode. 267 | 268 | [https://www.vaultproject.io/api-docs/secret/transform#encode](https://www.vaultproject.io/api-docs/secret/transform#encode) 269 | 270 | To encode data using transform secrets engine, you POST a JSON payload to the path `/v1/transform/encode/:role_name`, in this example `:role_name` is payments, which is the name of the role created earlier. 271 | 272 | The API requires that you have a valid Vault token, and that token has the right policy allocated to it to operate. The Vault token is sent to the request using the `X-Vault-Token` HTTP header. 273 | 274 | The payload for the request is a simple JSON structure with a single field `value`, you can see an example below 275 | 276 | ```json 277 | { 278 | "value": "1111-2222-3333-4444" 279 | } 280 | ``` 281 | 282 | If you were to use cURL to interact with the API and encode some data you could use the following command. You post the JSON payload to the path `v1/transform/encode/payments` along with the Vault token in an HTTP header. 283 | 284 | ```shell 285 | curl localhost:8200/v1/transform/encode/payments \ 286 | -H 'X-Vault-Token: root' \ 287 | -d '{"value": "1111-2222-3333-4444"}' 288 | ``` 289 | 290 | The ciphertext for the submitted data is returned in the JSON response at `.data.encoded_value`. As you will see later on in the post it is a fairly trivial exercise to extract this information. 291 | 292 | ```json 293 | { 294 | "request_id": "0f170922-d7c1-0137-391b-932a2025beb4", 295 | "lease_id": "", 296 | "renewable": false, 297 | "lease_duration": 0, 298 | "data": { 299 | "encoded_value": "1111-2208-4340-0589" 300 | }, 301 | "wrap_info": null, 302 | "warnings": null, 303 | "auth": null 304 | } 305 | ``` 306 | 307 | Now you understand the basics with interacting with the API let's see how this can be done from your applications code. 308 | 309 | ## Interacting with the Vault API from Java and Go 310 | 311 | The first thing we need to do is to construct a byte array which holds a JSON formatted string for your payload. 312 | 313 | 320 | 321 | 322 | 323 | ```go 324 | // create the JSON request as a byte array 325 | req := TokenRequest{Value: cc} 326 | data, _ := json.Marshal(req) 327 | ``` 328 | 329 | 330 | 331 | 332 | 333 | ```java 334 | // create the request 335 | TokenRequest req = new TokenRequest(cardNumber); 336 | 337 | // convert the POJO to a byte array 338 | ObjectMapper mapper = new ObjectMapper(); 339 | mapper.enable(SerializationFeature.INDENT_OUTPUT); 340 | byte[] byteRequest = mapper.writeValueAsBytes(req); 341 | ``` 342 | 343 | 344 | 345 | 346 | 347 | You can then construct the request, setting this payload as part of the request body. 348 | 349 | 356 | 357 | 358 | 359 | ```go 360 | url := fmt.Sprintf("http://%s/v1/transform/encode/payments", c.uri) 361 | r, _ := http.NewRequest(http.MethodPost, url, bytes.NewReader(data)) 362 | r.Header.Add("X-Vault-Token", "root") 363 | 364 | resp, err := http.DefaultClient.Do(r) 365 | if err != nil { 366 | return "", err 367 | } 368 | defer resp.Body.Close() 369 | 370 | if resp.StatusCode != http.StatusOK { 371 | return "", fmt.Errorf("Vault returned reponse code %d, expected status code 200", resp.StatusCode) 372 | } 373 | ``` 374 | 375 | 376 | 377 | 378 | 379 | ```java 380 | // make a call to vault to process the request 381 | URL url = new URL(this.url); 382 | HttpURLConnection con = (HttpURLConnection)url.openConnection(); 383 | con.setDoOutput(true); 384 | con.setRequestMethod("POST"); 385 | con.setRequestProperty("Content-Type", "application/json; utf-8"); 386 | con.setRequestProperty("Accept", "application/json"); 387 | con.setRequestProperty("X-Vault-Token", this.token); 388 | 389 | // write the body 390 | try(OutputStream os = con.getOutputStream()) { 391 | os.write(byteRequest, 0, byteRequest.length); 392 | } 393 | ``` 394 | 395 | 396 | 397 | 398 | 399 | To read the JSON response, you can parse the response body from the HTTP client into a simple structure. 400 | 401 | 408 | 409 | 410 | 411 | ```go 412 | // process the response 413 | tr := &TokenResponse{} 414 | err = json.NewDecoder(resp.Body).Decode(tr) 415 | if err != nil { 416 | return "", err 417 | } 418 | ``` 419 | 420 | 421 | 422 | 423 | 424 | ```java 425 | // read the response 426 | TokenResponse resp = new ObjectMapper() 427 | .readerFor(TokenResponse.class) 428 | .readValue(con.getInputStream()); 429 | ``` 430 | 431 | 432 | 433 | 434 | 435 | Full source code for both examples can be found at: [https://github.com/nicholasjackson/demo-vault/tree/master/transform](https://github.com/nicholasjackson/demo-vault/tree/master/transform) 436 | 437 | ## Testing the service 438 | 439 | Let's test the service, the demo has both the Java and the Go code running, so you can use `curl` to test it. 440 | 441 | 448 | 449 | 450 | 451 | ```shell 452 | curl payments-go.container.shipyard.run:9090 -H "content-type: application/json" -d '{"card_number": "1234-1234-1234-1234"}' 453 | ``` 454 | 455 | 456 | 457 | 458 | 459 | ```shell 460 | curl payments-java.container.shipyard.run:9090 -H "content-type: application/json" -d '{"card_number": "1234-1234-1234-1234"}' 461 | ``` 462 | 463 | 464 | 465 | 466 |

467 | 468 |

469 | 470 | You should see a response something like the following 471 | 472 | ```json 473 | {"transaction_id": 11} 474 | ``` 475 | 476 | If you query the orders table on the database you will be able to see the encrypted value for this transaction 477 | 478 | ``` 479 | PGPASSWORD=password psql -h localhost -p 5432 -U root -d payments -c 'SELECT * from orders;' 480 | ``` 481 | 482 |

483 | 484 |

485 | 486 | You can validate that this cyphertext is correct using the CLI like in the earlier example: 487 | 488 | ```shell 489 | vault write transform/decode/payments value= 490 | ``` 491 | 492 | 493 | 494 | ## Summary 495 | 496 | In this post, you have seen how the new Transform secrets engine can be used to partially encrypt credit card numbers at rest while preserving the formatting and ability to query the card issuer. 497 | 498 | This example only covers one of the possibilities for the Transform secrets engine; if you have an interesting use case for Transform let us know, we would love to feature this in a future post. -------------------------------------------------------------------------------- /transform/blueprint/files/agent_config.hcl: -------------------------------------------------------------------------------- 1 | auto_auth { 2 | method { 3 | type = "approle" 4 | config = { 5 | role_id_file_path = "/secrets/role_id" 6 | secret_id_file_path = "/secrets/secret_id" 7 | remove_secret_id_file_after_reading = true 8 | } 9 | } 10 | } 11 | 12 | cache { 13 | use_auto_auth_token = true 14 | } 15 | 16 | listener "tcp" { 17 | address = "127.0.0.1:8200" 18 | tls_disable = true 19 | } 20 | 21 | vault { 22 | address = "http://vault.container.shipyard.run:8200" 23 | } -------------------------------------------------------------------------------- /transform/blueprint/files/db_setup.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE orders ( 2 | id SERIAL PRIMARY KEY, 3 | card_number VARCHAR (255) NOT NULL, 4 | created_at TIMESTAMP NOT NULL, 5 | updated_at TIMESTAMP NOT NULL, 6 | deleted_at TIMESTAMP 7 | ); -------------------------------------------------------------------------------- /transform/blueprint/files/setup_approle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | vault auth enable approle 4 | 5 | vault write auth/approle/role/payments_app \ 6 | secret_id_ttl=10m \ 7 | token_num_uses=10 \ 8 | token_ttl=20m \ 9 | token_max_ttl=30m \ 10 | secret_id_num_uses=40 11 | 12 | output=$(dirname "$0")/../secrets/ 13 | 14 | # fetch the role-id from vault and parse the response 15 | vault read auth/approle/role/payments_app/role-id -format=json | sed -E -n 's/.*"role_id": "([^"]*).*/\1/p' > ${output}/role_id 16 | 17 | # fetch the secret from vault and parse the response 18 | vault write -f auth/approle/role/payments_app/secret-id -format=json | sed -E -n 's/.*"secret_id": "([^"]*).*/\1/p' > ${output}/secret_id -------------------------------------------------------------------------------- /transform/blueprint/files/setup_transform.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | vault secrets enable transform 4 | 5 | vault write transform/role/payments transformations=ccn-fpe 6 | 7 | vault write transform/transformation/ccn-fpe \ 8 | type=fpe \ 9 | template=ccn \ 10 | tweak_source=internal \ 11 | allowed_roles=payments 12 | 13 | vault write transform/template/ccn \ 14 | type=regex \ 15 | pattern='\d{4}-\d{2}(\d{2})-(\d{4})-(\d{4})' \ 16 | alphabet=numerics 17 | 18 | vault write transform/alphabet/numerics \ 19 | alphabet="0123456789" -------------------------------------------------------------------------------- /transform/blueprint/network.hcl: -------------------------------------------------------------------------------- 1 | network "local" { 2 | subnet = "10.5.0.0/16" 3 | } -------------------------------------------------------------------------------- /transform/blueprint/postgres.hcl: -------------------------------------------------------------------------------- 1 | container "postgres" { 2 | image { 3 | name = "postgres:11.6" 4 | } 5 | 6 | port { 7 | local = 5432 8 | remote = 5432 9 | host = 5432 10 | } 11 | 12 | env { 13 | key = "POSTGRES_DB" 14 | value = "payments" 15 | } 16 | 17 | env { 18 | key = "POSTGRES_USER" 19 | value = "root" 20 | } 21 | 22 | env { 23 | key = "POSTGRES_PASSWORD" 24 | value = "password" 25 | } 26 | 27 | # Mount the volume for the DB setup script which runs when 28 | # the container starts 29 | volume { 30 | source = "./files/db_setup.sql" 31 | destination = "/docker-entrypoint-initdb.d/db_setup.sql" 32 | } 33 | 34 | network { 35 | name = "network.local" 36 | } 37 | } -------------------------------------------------------------------------------- /transform/blueprint/secrets/role_id: -------------------------------------------------------------------------------- 1 | 929a5f41-555c-e481-cce9-1a17d970e921 2 | -------------------------------------------------------------------------------- /transform/blueprint/secrets/secret_id: -------------------------------------------------------------------------------- 1 | 98c30678-dbb3-9d81-4be3-1d608b56afa2 2 | -------------------------------------------------------------------------------- /transform/blueprint/vault.hcl: -------------------------------------------------------------------------------- 1 | container "vault" { 2 | image { 3 | name = "hashicorp/vault-enterprise:1.4.0-rc1_ent" 4 | } 5 | 6 | command = [ 7 | "vault", 8 | "server", 9 | "-dev", 10 | "-dev-root-token-id=root", 11 | "-dev-listen-address=0.0.0.0:8200", 12 | ] 13 | 14 | port { 15 | local = 8200 16 | remote = 8200 17 | host = 8200 18 | } 19 | 20 | # Wait for Vault to start 21 | health_check { 22 | timeout = "30s" 23 | http = "http://localhost:8200/v1/sys/health" 24 | } 25 | 26 | volume { 27 | source = "./files" 28 | destination = "/files" 29 | } 30 | 31 | env { 32 | key = "VAULT_ADDR" 33 | value = "http://localhost:8200" 34 | } 35 | 36 | env { 37 | key = "VAULT_TOKEN" 38 | value = "root" 39 | } 40 | 41 | network { 42 | name = "network.local" 43 | } 44 | } 45 | 46 | #exec_remote "setup" { 47 | # target = "container.vault" 48 | # 49 | # cmd = "/files/setup_vault.sh" 50 | #} -------------------------------------------------------------------------------- /transform/transform-engine-go/.realize.yaml: -------------------------------------------------------------------------------- 1 | settings: 2 | legacy: 3 | force: false 4 | interval: 0s 5 | server: 6 | status: true 7 | open: false 8 | host: 0.0.0.0 9 | port: 5002 10 | schema: 11 | - name: files 12 | path: . 13 | commands: 14 | install: 15 | status: true 16 | method: go build -o ./bin/server 17 | run: 18 | status: true 19 | method: ./bin/server 20 | watcher: 21 | paths: 22 | - / 23 | extensions: 24 | - go 25 | ignored_paths: 26 | - .git 27 | - .realize -------------------------------------------------------------------------------- /transform/transform-engine-go/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.13 as build 2 | 3 | 4 | WORKDIR /go/src/github.com/nicholasjackson/demo-vault/transform-engine-go 5 | COPY . . 6 | 7 | RUN go get -d -v ./... && \ 8 | CGO_ENABLED=0 go build -o /bin/server ./ 9 | 10 | FROM alpine:latest 11 | 12 | RUN apk add curl 13 | 14 | RUN mkdir /app 15 | COPY --from=build /bin/server /app/server 16 | 17 | ENTRYPOINT [ "/app/server" ] -------------------------------------------------------------------------------- /transform/transform-engine-go/bin/server: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholasjackson/demo-vault/0e6bd10b886fb72b83f017895dfa4d2c9eac120c/transform/transform-engine-go/bin/server -------------------------------------------------------------------------------- /transform/transform-engine-go/data/postgres.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/jmoiron/sqlx" 9 | "github.com/lib/pq" 10 | ) 11 | 12 | // Order defines an order object which is stored in the DB orders table 13 | type Order struct { 14 | ID int `db:"id" json:"id"` 15 | CardNumber string `db:"card_number" json:"card_number"` 16 | CreatedAt string `db:"created_at" json:"-"` 17 | UpdatedAt string `db:"updated_at" json:"-"` 18 | DeletedAt sql.NullString `db:"deleted_at" json:"-"` 19 | } 20 | 21 | // PostgreSQL is a database client for PostgresSQL 22 | type PostgreSQL struct { 23 | db *sqlx.DB 24 | } 25 | 26 | // NewPostgreSQLClient creates a new SQL client 27 | func NewPostgreSQLClient(connection string) (*PostgreSQL, error) { 28 | db, err := sqlx.Connect("postgres", connection) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | ps := &PostgreSQL{db} 34 | _, err = ps.IsConnected() 35 | 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return ps, nil 41 | } 42 | 43 | // IsConnected checks the connection to the database and returns an error if not connected 44 | func (c *PostgreSQL) IsConnected() (bool, error) { 45 | err := c.db.Ping() 46 | if err != nil { 47 | return false, err 48 | } 49 | 50 | return true, nil 51 | } 52 | 53 | // SaveOrder saves the order into the datbase 54 | func (c *PostgreSQL) SaveOrder(o Order) (int64, error) { 55 | var id int64 56 | 57 | err := c.db.QueryRow( 58 | `INSERT INTO orders (card_number, created_at, updated_at) VALUES ($1, $2, $3) RETURNING id`, 59 | o.CardNumber, 60 | pq.FormatTimestamp(time.Now()), 61 | pq.FormatTimestamp(time.Now()), 62 | ).Scan(&id) 63 | 64 | if err != nil { 65 | return -1, fmt.Errorf("Unable to insert record into DB. Error: %s", err) 66 | } 67 | 68 | fmt.Println("id", id) 69 | 70 | return id, nil 71 | } 72 | -------------------------------------------------------------------------------- /transform/transform-engine-go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nicholasjackson/demo-vault/transform-engine-go 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/google/martian v2.1.0+incompatible // indirect 7 | github.com/gorilla/mux v1.7.4 8 | github.com/hajimehoshi/oto v0.5.4 9 | github.com/hashicorp/go-hclog v0.12.2 10 | github.com/jmoiron/sqlx v1.2.0 11 | github.com/lib/pq v1.3.0 12 | github.com/nicholasjackson/env v0.6.0 13 | golang.org/x/sys v0.0.0-20191010194322-b09406accb47 // indirect 14 | google.golang.org/appengine v1.6.5 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /transform/transform-engine-go/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 4 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 5 | github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= 6 | github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 7 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 8 | github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= 9 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 10 | github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= 11 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 12 | github.com/hajimehoshi/oto v0.5.4 h1:Dn+WcYeF310xqStKm0tnvoruYUV5Sce8+sfUaIvWGkE= 13 | github.com/hajimehoshi/oto v0.5.4/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= 14 | github.com/hashicorp/go-hclog v0.12.2 h1:F1fdYblUEsxKiailtkhCCG2g4bipEgaHiDc8vffNpD4= 15 | github.com/hashicorp/go-hclog v0.12.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= 16 | github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= 17 | github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= 18 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 19 | github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= 20 | github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 21 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= 22 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 23 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 24 | github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= 25 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= 26 | github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= 27 | github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 28 | github.com/nicholasjackson/demo-vault v0.0.0-20200103191244-3870cf29c73c h1:/d/IX3ovqMe0QOIu1wPNc27auMYZ6XL2FDR3NIhRRwo= 29 | github.com/nicholasjackson/env v0.6.0 h1:6xdio52m7cKRtgZPER6NFeBZxicR88rx5a+5Jl4/qus= 30 | github.com/nicholasjackson/env v0.6.0/go.mod h1:/GtSb9a/BDUCLpcnpauN0d/Bw5ekSI1vLC1b9Lw0Vyk= 31 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 32 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 33 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 34 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 35 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 36 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 37 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 38 | golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 39 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 40 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 41 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 42 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 43 | golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 44 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 45 | golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY= 46 | golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 47 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 48 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 49 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 50 | google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= 51 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 52 | -------------------------------------------------------------------------------- /transform/transform-engine-go/handlers/health.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/hashicorp/go-hclog" 8 | "github.com/nicholasjackson/demo-vault/transform-engine-go/data" 9 | "github.com/nicholasjackson/demo-vault/transform-engine-go/vault" 10 | ) 11 | 12 | // Health is a http.Handler for API status 13 | type Health struct { 14 | pc *data.PostgreSQL 15 | vc *vault.Client 16 | log hclog.Logger 17 | } 18 | 19 | // NewHealth creates a new health handler 20 | func NewHealth(pc *data.PostgreSQL, vc *vault.Client, log hclog.Logger) *Health { 21 | return &Health{pc, vc, log} 22 | } 23 | 24 | // HealthResponse is returned by the handler 25 | type HealthResponse struct { 26 | Vault string `json:"vault"` 27 | DB string `json:"db"` 28 | } 29 | 30 | func (h *Health) ServeHTTP(rw http.ResponseWriter, r *http.Request) { 31 | hr := &HealthResponse{ 32 | Vault: "OK", 33 | DB: "OK", 34 | } 35 | 36 | status := http.StatusOK 37 | 38 | // check health of Vault 39 | if ok, _ := h.pc.IsConnected(); !ok { 40 | hr.DB = "Fail" 41 | status = http.StatusInternalServerError 42 | } 43 | 44 | if !h.vc.IsOK() { 45 | hr.Vault = "Fail" 46 | status = http.StatusInternalServerError 47 | } 48 | 49 | rw.WriteHeader(status) 50 | json.NewEncoder(rw).Encode(hr) 51 | 52 | } 53 | -------------------------------------------------------------------------------- /transform/transform-engine-go/handlers/payment.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/hashicorp/go-hclog" 9 | "github.com/nicholasjackson/demo-vault/transform-engine-go/data" 10 | "github.com/nicholasjackson/demo-vault/transform-engine-go/vault" 11 | ) 12 | 13 | // Payment is a http.Handler for payment routes 14 | type Payment struct { 15 | pc *data.PostgreSQL 16 | vc *vault.Client 17 | log hclog.Logger 18 | } 19 | 20 | // NewPayment creates a new payment handler 21 | func NewPayment(pc *data.PostgreSQL, vc *vault.Client, log hclog.Logger) *Payment { 22 | return &Payment{pc, vc, log} 23 | } 24 | 25 | // PaymentRequest is sent as part of the HTTP request 26 | type PaymentRequest struct { 27 | CardNumber string `json:"card_number"` 28 | Expiration string `json:"expiration"` 29 | CV2 string `json:"cv2"` 30 | } 31 | 32 | // PaymentResponse is sent with a successful payment 33 | type PaymentResponse struct { 34 | TransactionID int64 `json:"transaction_id"` 35 | } 36 | 37 | // PaymentError is returned when an error occurs 38 | type PaymentError struct { 39 | Message string `json:"message"` 40 | } 41 | 42 | // ServeHTTP implement http.Handler interface 43 | func (p *Payment) ServeHTTP(rw http.ResponseWriter, r *http.Request) { 44 | // parse the request 45 | pr := &PaymentRequest{} 46 | err := json.NewDecoder(r.Body).Decode(pr) 47 | if err != nil { 48 | p.log.Error("Unable to parse request", "error", err) 49 | 50 | presp := &PaymentError{fmt.Sprintf("Unable to parse request: %s", err)} 51 | rw.WriteHeader(http.StatusBadRequest) 52 | json.NewEncoder(rw).Encode(presp) 53 | return 54 | } 55 | 56 | // tokenize the card number 57 | encoded, err := p.vc.TokenizeCCNumber(pr.CardNumber) 58 | if err != nil { 59 | p.log.Error("Unable to tokenize record", "error", err) 60 | 61 | presp := &PaymentError{fmt.Sprintf("Uable to tokenize record: %s", err)} 62 | rw.WriteHeader(http.StatusInternalServerError) 63 | json.NewEncoder(rw).Encode(presp) 64 | return 65 | } 66 | 67 | // save the data into the database 68 | o := data.Order{CardNumber: encoded} 69 | id, err := p.pc.SaveOrder(o) 70 | if err != nil { 71 | p.log.Error("Unable to save record", "error", err) 72 | 73 | presp := &PaymentError{fmt.Sprintf("Uable to save data to db: %s", err)} 74 | rw.WriteHeader(http.StatusInternalServerError) 75 | json.NewEncoder(rw).Encode(presp) 76 | return 77 | } 78 | 79 | presp := PaymentResponse{id} 80 | json.NewEncoder(rw).Encode(presp) 81 | } 82 | 83 | // Example VISA number 84 | //o := data.Order{CardNumber: "4024-2322-1235-9245"} 85 | // Example MasterCard number 86 | //o = data.Order{CardNumber: "5355-6853-9451-3461"} 87 | -------------------------------------------------------------------------------- /transform/transform-engine-go/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/hashicorp/go-hclog" 10 | "github.com/nicholasjackson/demo-vault/transform-engine-go/data" 11 | "github.com/nicholasjackson/demo-vault/transform-engine-go/handlers" 12 | "github.com/nicholasjackson/demo-vault/transform-engine-go/vault" 13 | "github.com/nicholasjackson/env" 14 | ) 15 | 16 | var postgresHost = env.String("POSTGRES_HOST", false, "localhost", "PostgreSQL server location") 17 | var postgresPort = env.Int("POSTGRES_PORT", false, 5432, "PostgreSQL port") 18 | var vaultHost = env.String("VAULT_ADDR", false, "http://localhost:8200", "Vault server location") 19 | 20 | func main() { 21 | env.Parse() 22 | 23 | var err error 24 | log := hclog.Default() 25 | 26 | // Create the postgres DB client 27 | var pc *data.PostgreSQL 28 | for { 29 | pc, err = data.NewPostgreSQLClient(fmt.Sprintf("host=%s port=%d user=root password=password dbname=payments sslmode=disable", *postgresHost, *postgresPort)) 30 | if err == nil { 31 | break 32 | } 33 | 34 | log.Error("Unable to connect to database", "error", err) 35 | time.Sleep(5 * time.Second) 36 | } 37 | 38 | // Create the Vault client 39 | vc := vault.NewClient("root", *vaultHost) 40 | 41 | // create the HTTP handler 42 | ph := handlers.NewPayment(pc, vc, log) 43 | hh := handlers.NewHealth(pc, vc, log) 44 | 45 | r := mux.NewRouter() 46 | r.Handle("/", ph).Methods(http.MethodPost) 47 | r.Handle("/health", hh).Methods(http.MethodGet) 48 | 49 | log.Info("Starting server", "bind", ":9090") 50 | 51 | http.ListenAndServe(":9090", r) 52 | } 53 | -------------------------------------------------------------------------------- /transform/transform-engine-go/vault/vault.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | type Client struct { 11 | token string 12 | uri string 13 | } 14 | 15 | type TokenRequest struct { 16 | Value string `json:"value"` 17 | } 18 | 19 | type TokenResponse struct { 20 | Data TokenReponseData `json:"data"` 21 | } 22 | 23 | type TokenReponseData struct { 24 | EncodedValue string `json:"encoded_value"` 25 | } 26 | 27 | // NewClient creates a new Vault client 28 | func NewClient(token, serverURI string) *Client { 29 | return &Client{token, serverURI} 30 | } 31 | 32 | // IsOK returns true if Vault is unsealed and can accept requests 33 | func (c *Client) IsOK() bool { 34 | url := fmt.Sprintf("%s/v1/sys/health", c.uri) 35 | 36 | r, _ := http.NewRequest(http.MethodGet, url, nil) 37 | r.Header.Add("X-Vault-Token", c.token) 38 | 39 | resp, err := http.DefaultClient.Do(r) 40 | if err != nil { 41 | return false 42 | } 43 | 44 | defer resp.Body.Close() 45 | 46 | if resp.StatusCode != http.StatusOK { 47 | return false 48 | } 49 | 50 | return true 51 | } 52 | 53 | // TokenizeCCNumber uses the Vault API to tokenize the given string 54 | func (c *Client) TokenizeCCNumber(cc string) (string, error) { 55 | // create the JSON request as a byte array 56 | req := TokenRequest{Value: cc} 57 | data, _ := json.Marshal(req) 58 | 59 | // call the api 60 | url := fmt.Sprintf("%s/v1/transform/encode/payments", c.uri) 61 | r, _ := http.NewRequest(http.MethodPost, url, bytes.NewReader(data)) 62 | r.Header.Add("X-Vault-Token", c.token) 63 | 64 | resp, err := http.DefaultClient.Do(r) 65 | if err != nil { 66 | return "", err 67 | } 68 | defer resp.Body.Close() 69 | 70 | if resp.StatusCode != http.StatusOK { 71 | return "", fmt.Errorf("Vault returned reponse code %d, expected status code 200", resp.StatusCode) 72 | } 73 | 74 | // process the response 75 | tr := &TokenResponse{} 76 | err = json.NewDecoder(resp.Body).Decode(tr) 77 | if err != nil { 78 | return "", err 79 | } 80 | 81 | return tr.Data.EncodedValue, nil 82 | } 83 | -------------------------------------------------------------------------------- /transform/transform-engine-java/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /transform/transform-engine-java/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | transform-engine-java 4 | Project transform-engine-java created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.buildship.core.gradleprojectbuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.buildship.core.gradleprojectnature 22 | 23 | 24 | -------------------------------------------------------------------------------- /transform/transform-engine-java/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | arguments= 2 | auto.sync=false 3 | build.scans.enabled=false 4 | connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) 5 | connection.project.dir= 6 | eclipse.preferences.version=1 7 | gradle.user.home= 8 | java.home=/usr/lib/jvm/java-11-openjdk-amd64 9 | jvm.arguments= 10 | offline.mode=false 11 | override.workspace.settings=true 12 | show.console.view=true 13 | show.executions.view=true 14 | -------------------------------------------------------------------------------- /transform/transform-engine-java/.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 3 | org.eclipse.jdt.core.compiler.compliance=1.8 4 | org.eclipse.jdt.core.compiler.source=1.8 5 | -------------------------------------------------------------------------------- /transform/transform-engine-java/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:12 as build 2 | 3 | WORKDIR /src 4 | COPY . /src 5 | 6 | RUN ./gradlew build 7 | 8 | FROM openjdk:12 9 | 10 | RUN mkdir /app 11 | WORKDIR /app 12 | 13 | COPY --from=build /src/build/libs/spring-boot-payments-0.1.0.jar /app/spring-boot-payments-0.1.0.jar 14 | 15 | ENTRYPOINT ["java", "-jar", "/app/spring-boot-payments-0.1.0.jar"] -------------------------------------------------------------------------------- /transform/transform-engine-java/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | dependencies { 6 | classpath("org.springframework.boot:spring-boot-gradle-plugin:2.2.6.RELEASE") 7 | } 8 | } 9 | 10 | apply plugin: 'java' 11 | apply plugin: 'eclipse' 12 | apply plugin: 'idea' 13 | apply plugin: 'org.springframework.boot' 14 | apply plugin: 'io.spring.dependency-management' 15 | 16 | bootJar { 17 | baseName = 'spring-boot-payments' 18 | version = '0.1.0' 19 | } 20 | 21 | repositories { 22 | mavenCentral() 23 | } 24 | 25 | sourceCompatibility = 1.8 26 | targetCompatibility = 1.8 27 | 28 | dependencies { 29 | compile("org.springframework.boot:spring-boot-starter-web") 30 | compile('org.springframework.boot:spring-boot-starter-log4j2') 31 | 32 | compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.10.3' 33 | compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.10.3' 34 | compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.10.3' 35 | 36 | 37 | implementation('org.springframework.boot:spring-boot-starter-data-jpa') 38 | implementation('org.postgresql:postgresql') 39 | 40 | testCompile("junit:junit") 41 | } 42 | 43 | configurations { 44 | all { 45 | exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' 46 | } 47 | } -------------------------------------------------------------------------------- /transform/transform-engine-java/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholasjackson/demo-vault/0e6bd10b886fb72b83f017895dfa4d2c9eac120c/transform/transform-engine-java/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /transform/transform-engine-java/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /transform/transform-engine-java/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /transform/transform-engine-java/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | @rem Execute Gradle 88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 89 | 90 | :end 91 | @rem End local scope for the variables with windows NT shell 92 | if "%ERRORLEVEL%"=="0" goto mainEnd 93 | 94 | :fail 95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 96 | rem the _cmd.exe /c_ return code! 97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 98 | exit /b 1 99 | 100 | :mainEnd 101 | if "%OS%"=="Windows_NT" endlocal 102 | 103 | :omega 104 | -------------------------------------------------------------------------------- /transform/transform-engine-java/src/main/java/payments/Application.java: -------------------------------------------------------------------------------- 1 | package payments; 2 | 3 | import java.util.Arrays; 4 | 5 | import org.springframework.boot.CommandLineRunner; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.context.ApplicationContext; 9 | import org.springframework.context.annotation.Bean; 10 | 11 | @SpringBootApplication 12 | public class Application { 13 | 14 | public static void main(String[] args) { 15 | SpringApplication.run(Application.class, args); 16 | } 17 | 18 | @Bean 19 | public CommandLineRunner commandLineRunner(ApplicationContext ctx) { 20 | return args -> { 21 | 22 | System.out.println("Let's inspect the beans provided by Spring Boot:"); 23 | 24 | String[] beanNames = ctx.getBeanDefinitionNames(); 25 | Arrays.sort(beanNames); 26 | for (String beanName : beanNames) { 27 | System.out.println(beanName); 28 | } 29 | 30 | }; 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /transform/transform-engine-java/src/main/java/payments/HealthResponse.java: -------------------------------------------------------------------------------- 1 | package payments; 2 | 3 | import com.fasterxml.jackson.annotation.JsonGetter; 4 | 5 | public class HealthResponse { 6 | private String vault; 7 | private String db; 8 | 9 | @JsonGetter("vault") 10 | public String getVault() { 11 | return vault; 12 | } 13 | 14 | @JsonGetter("db") 15 | public String getDB() { 16 | return db; 17 | } 18 | 19 | HealthResponse(String vault, String db) { 20 | this.db = db; 21 | this.vault = vault; 22 | } 23 | } -------------------------------------------------------------------------------- /transform/transform-engine-java/src/main/java/payments/PaymentRequest.java: -------------------------------------------------------------------------------- 1 | package payments; 2 | 3 | import com.fasterxml.jackson.annotation.JsonGetter; 4 | import com.fasterxml.jackson.annotation.JsonSetter; 5 | 6 | class PaymentRequest { 7 | 8 | private String cardNumber; 9 | private String expiration; 10 | private String cv2; 11 | 12 | @JsonGetter("card_number") 13 | public String getCardNumber() { 14 | return this.cardNumber; 15 | } 16 | 17 | @JsonSetter("card_number") 18 | public void setCardNumber(final String number) { 19 | this.cardNumber = number; 20 | } 21 | 22 | @JsonGetter("expiration") 23 | public String getExpiration() { 24 | return this.expiration; 25 | } 26 | 27 | @JsonSetter("expiration") 28 | public void setExpiration(final String exp) { 29 | this.expiration = exp; 30 | } 31 | 32 | @JsonGetter("cv2") 33 | public String getCV2() { 34 | return this.cv2; 35 | } 36 | 37 | @JsonSetter("cv2") 38 | public void setCV2(final String cv2) { 39 | this.cv2 = cv2; 40 | } 41 | } -------------------------------------------------------------------------------- /transform/transform-engine-java/src/main/java/payments/PaymentResponse.java: -------------------------------------------------------------------------------- 1 | package payments; 2 | 3 | import java.util.UUID; 4 | 5 | import com.fasterxml.jackson.annotation.JsonGetter; 6 | 7 | class PaymentResponse { 8 | 9 | private Integer transactionid; 10 | 11 | @JsonGetter("transaction_id") 12 | public Integer getId() { 13 | return transactionid; 14 | } 15 | 16 | PaymentResponse(Integer id) { 17 | this.transactionid = id; 18 | } 19 | } -------------------------------------------------------------------------------- /transform/transform-engine-java/src/main/java/payments/PaymentsController.java: -------------------------------------------------------------------------------- 1 | package payments; 2 | 3 | import java.io.IOException; 4 | import java.sql.SQLException; 5 | 6 | import javax.sql.DataSource; 7 | 8 | import java.sql.Connection; 9 | 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.core.env.Environment; 14 | import org.springframework.web.bind.annotation.GetMapping; 15 | import org.springframework.web.bind.annotation.PostMapping; 16 | import org.springframework.web.bind.annotation.RequestBody; 17 | import org.springframework.web.bind.annotation.ResponseBody; 18 | import org.springframework.web.bind.annotation.RestController; 19 | 20 | import payments.model.Order; 21 | import payments.repository.OrderRepository; 22 | import payments.vault.VaultClient; 23 | 24 | @RestController 25 | public class PaymentsController { 26 | @Autowired 27 | OrderRepository repository; 28 | 29 | @Autowired 30 | DataSource dataSource; 31 | 32 | private Environment env; 33 | 34 | Logger logger = LoggerFactory.getLogger(PaymentsController.class); 35 | VaultClient vaultClient; 36 | 37 | PaymentsController(Environment env) { 38 | this.env = env; 39 | vaultClient = new VaultClient(env.getProperty("vault.token"), env.getProperty("vault.addr")); 40 | } 41 | 42 | @PostMapping("/") 43 | @ResponseBody 44 | public PaymentResponse pay(@RequestBody PaymentRequest request) throws IOException { 45 | // tokenize the thing 46 | String tokenizedNumber = vaultClient.TokenizeCCNumber(request.getCardNumber()); 47 | 48 | /* 49 | * Those who came before me 50 | * Lived through their vocations 51 | * From the past until completion 52 | * They'll turn away no more 53 | * And I still find it so hard 54 | * To say what I need to say 55 | * But I'm quite sure that you'll tell me 56 | * Just how I should feel today 57 | */ 58 | Order newOrder = repository.save(new Order(tokenizedNumber)); 59 | 60 | logger.info("New payment"); 61 | logger.info("CC Number: {}", request.getCardNumber()); 62 | logger.info("Tokenized Number: {}", tokenizedNumber); 63 | logger.info("DB record ID: {}", newOrder.getId()); 64 | 65 | return new PaymentResponse(newOrder.getId()); 66 | } 67 | 68 | @GetMapping("/health") 69 | @ResponseBody 70 | public HealthResponse health() throws IOException { 71 | logger.info("Health Check"); 72 | 73 | String dbHealth = "OK"; 74 | String vaultHealth = "OK"; 75 | 76 | try { 77 | Connection con = dataSource.getConnection(); 78 | 79 | if (con.isClosed()) { 80 | dbHealth = "Fail"; 81 | } 82 | } catch (SQLException e) { 83 | dbHealth = "Fail"; 84 | } 85 | 86 | if (!vaultClient.IsOK()) { 87 | vaultHealth = "Fail"; 88 | } 89 | 90 | return new HealthResponse(vaultHealth, dbHealth); 91 | } 92 | 93 | } -------------------------------------------------------------------------------- /transform/transform-engine-java/src/main/java/payments/model/Order.java: -------------------------------------------------------------------------------- 1 | package payments.model; 2 | 3 | import java.io.Serializable; 4 | import java.math.BigInteger; 5 | import java.time.LocalDateTime; 6 | 7 | import javax.persistence.Column; 8 | import javax.persistence.Entity; 9 | import javax.persistence.GeneratedValue; 10 | import javax.persistence.GenerationType; 11 | import javax.persistence.Id; 12 | import javax.persistence.Table; 13 | 14 | @Entity 15 | @Table(name = "orders") 16 | public class Order implements Serializable { 17 | 18 | private static final long serialVersionUID = -2343243243242432341L; 19 | @Id 20 | @GeneratedValue(strategy = GenerationType.IDENTITY) 21 | private Integer id; 22 | 23 | @Column(name = "card_number") 24 | private String cardNumber; 25 | 26 | @Column(name = "created_at") 27 | private LocalDateTime createdAt; 28 | 29 | @Column(name = "updated_at") 30 | private LocalDateTime updatedAt; 31 | 32 | @Column(name = "deleted_at") 33 | private LocalDateTime deletedAt; 34 | 35 | protected Order() { 36 | } 37 | 38 | public Order(String cardNumber) { 39 | this.cardNumber = cardNumber; 40 | this.createdAt = LocalDateTime.now(); 41 | this.updatedAt = LocalDateTime.now(); 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return String.format("Customer[id=%d, cardNumber='%s']", id, cardNumber); 47 | } 48 | 49 | public Integer getId() { 50 | return this.id; 51 | } 52 | 53 | public String getCardNumber() { 54 | return cardNumber; 55 | } 56 | 57 | public void setCardNumber(String cardNumber) { 58 | this.cardNumber = cardNumber; 59 | } 60 | 61 | public LocalDateTime getCreatedAt() { 62 | return createdAt; 63 | } 64 | 65 | public void setCreatedAt(LocalDateTime createdAt) { 66 | this.createdAt = createdAt; 67 | } 68 | 69 | public LocalDateTime getUpdatedAt() { 70 | return updatedAt; 71 | } 72 | 73 | public void setUpdatedAt(LocalDateTime updatedAt) { 74 | this.updatedAt = updatedAt; 75 | } 76 | } -------------------------------------------------------------------------------- /transform/transform-engine-java/src/main/java/payments/repository/OrderRepository.java: -------------------------------------------------------------------------------- 1 | package payments.repository; 2 | 3 | import java.util.List; 4 | 5 | import payments.model.Order; 6 | 7 | import org.springframework.data.repository.CrudRepository; 8 | 9 | public interface OrderRepository extends CrudRepository{ 10 | List findById(int ID); 11 | List findAll(); 12 | } -------------------------------------------------------------------------------- /transform/transform-engine-java/src/main/java/payments/vault/TokenRequest.java: -------------------------------------------------------------------------------- 1 | package payments.vault; 2 | 3 | import com.fasterxml.jackson.annotation.JsonGetter; 4 | import com.fasterxml.jackson.annotation.JsonSetter; 5 | 6 | public class TokenRequest { 7 | private String value; 8 | 9 | public TokenRequest(final String value) { 10 | super(); 11 | 12 | this.value = value; 13 | } 14 | 15 | public void setValue(final String value) { 16 | this.value = value; 17 | } 18 | 19 | public String getValue() { 20 | return this.value; 21 | } 22 | } -------------------------------------------------------------------------------- /transform/transform-engine-java/src/main/java/payments/vault/TokenResponse.java: -------------------------------------------------------------------------------- 1 | package payments.vault; 2 | 3 | import com.fasterxml.jackson.annotation.JsonSetter; 4 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 5 | 6 | @JsonIgnoreProperties(ignoreUnknown = true) 7 | public class TokenResponse { 8 | private TokenResponseData data; 9 | 10 | public TokenResponse() { 11 | super(); 12 | } 13 | 14 | public TokenResponse(TokenResponseData data) { 15 | super(); 16 | 17 | this.data = data; 18 | } 19 | 20 | public TokenResponseData getTokenResponseData() { 21 | return this.data; 22 | } 23 | 24 | @JsonSetter("data") 25 | public void setTokenResponseData(TokenResponseData data) { 26 | this.data = data; 27 | } 28 | 29 | @JsonIgnoreProperties(ignoreUnknown = true) 30 | public class TokenResponseData { 31 | private String encodedValue; 32 | 33 | public TokenResponseData() { 34 | super(); 35 | } 36 | 37 | public TokenResponseData(String value) { 38 | super(); 39 | 40 | this.encodedValue = value; 41 | } 42 | 43 | public String getEncodedValue() { 44 | return this.encodedValue; 45 | } 46 | 47 | @JsonSetter("encoded_value") 48 | public void setEncodedValue(String value) { 49 | this.encodedValue = value; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /transform/transform-engine-java/src/main/java/payments/vault/VaultClient.java: -------------------------------------------------------------------------------- 1 | package payments.vault; 2 | 3 | import java.io.IOException; 4 | import java.net.HttpURLConnection; 5 | import java.net.URL; 6 | import java.io.OutputStream; 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import com.fasterxml.jackson.core.JsonProcessingException; 12 | import com.fasterxml.jackson.databind.ObjectMapper; 13 | import com.fasterxml.jackson.databind.SerializationFeature; 14 | 15 | public class VaultClient { 16 | private String token; 17 | private String serverURI; 18 | 19 | Logger logger = LoggerFactory.getLogger(VaultClient.class); 20 | 21 | public VaultClient(String token, String serverURI) { 22 | this.token = token; 23 | this.serverURI = serverURI; 24 | } 25 | 26 | public Boolean IsOK() throws IOException { 27 | URL url = new URL(this.serverURI+ "/v1/sys/health"); 28 | HttpURLConnection con = (HttpURLConnection)url.openConnection(); 29 | con.setRequestMethod("GET"); 30 | con.setRequestProperty("X-Vault-Token", this.token); 31 | 32 | int status = con.getResponseCode(); 33 | if (status != 200) { 34 | return false; 35 | } 36 | 37 | return true; 38 | } 39 | 40 | 41 | public String TokenizeCCNumber(String cardNumber) throws IOException, JsonProcessingException { 42 | // create the request 43 | TokenRequest req = new TokenRequest(cardNumber); 44 | 45 | // convert the POJO to a byte array 46 | ObjectMapper mapper = new ObjectMapper(); 47 | mapper.enable(SerializationFeature.INDENT_OUTPUT); 48 | byte[] byteRequest = mapper.writeValueAsBytes(req); 49 | 50 | // make a call to vault to process the request 51 | URL url = new URL(this.serverURI+ "/v1/transform/encode/payments"); 52 | HttpURLConnection con = (HttpURLConnection)url.openConnection(); 53 | con.setDoOutput(true); 54 | con.setRequestMethod("POST"); 55 | con.setRequestProperty("Content-Type", "application/json; utf-8"); 56 | con.setRequestProperty("Accept", "application/json"); 57 | con.setRequestProperty("X-Vault-Token", this.token); 58 | 59 | // write the body 60 | try(OutputStream os = con.getOutputStream()) { 61 | os.write(byteRequest, 0, byteRequest.length); 62 | } 63 | 64 | // read the response 65 | TokenResponse resp = new ObjectMapper() 66 | .readerFor(TokenResponse.class) 67 | .readValue(con.getInputStream()); 68 | 69 | return resp.getTokenResponseData().getEncodedValue(); 70 | } 71 | } -------------------------------------------------------------------------------- /transform/transform-engine-java/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 9090 3 | 4 | vault: 5 | addr: http://localhost:8200 6 | token: root 7 | 8 | spring: 9 | jpa: 10 | database: POSTGRESQL 11 | show-sql: true 12 | generate-ddl: true 13 | properties: 14 | hibernate: 15 | jdbc: 16 | lob: 17 | non_contextual_creation: true 18 | datasource: 19 | platform: postgres 20 | url: jdbc:postgresql://localhost:5432/payments 21 | username: root 22 | password: password --------------------------------------------------------------------------------