├── service ├── .ruby-version ├── .gitignore ├── api │ ├── lambda │ │ ├── create-bastion │ │ │ ├── .gitignore │ │ │ ├── outputs.tf │ │ │ ├── variables.tf │ │ │ └── main.tf │ │ ├── destroy-bastion │ │ │ ├── .gitignore │ │ │ ├── variables.tf │ │ │ ├── outputs.tf │ │ │ └── main.tf │ │ ├── trigger-bastion-destruction │ │ │ ├── resources │ │ │ │ └── export.js │ │ │ ├── .gitignore │ │ │ ├── variables.tf │ │ │ ├── outputs.tf │ │ │ ├── package.json │ │ │ ├── bin │ │ │ │ └── build.sh │ │ │ ├── project.clj │ │ │ ├── src │ │ │ │ └── trigger_bastion_destruction │ │ │ │ │ └── handler.cljs │ │ │ ├── main.tf │ │ │ └── package-lock.json │ │ └── bastion │ │ │ ├── .gitignore │ │ │ ├── src │ │ │ └── bastion │ │ │ │ ├── clients.clj │ │ │ │ ├── env.clj │ │ │ │ ├── destroy.clj │ │ │ │ ├── util.clj │ │ │ │ ├── eni.clj │ │ │ │ ├── create.clj │ │ │ │ ├── task.clj │ │ │ │ └── sg.clj │ │ │ └── project.clj │ ├── outputs.tf │ ├── variables.tf │ ├── main.tf │ └── bastion.tf ├── create.rb ├── destroy.rb ├── outputs.tf ├── Gemfile ├── variables.tf ├── bin │ ├── build.sh │ ├── destroy-bastion.sh │ └── create-bastion.sh ├── container-definitions.tpl.json ├── Gemfile.lock ├── session.rb └── main.tf ├── vpc ├── variables.tf ├── outputs.tf └── main.tf ├── bastion ├── variables.tf ├── outputs.tf ├── bin │ ├── push.sh │ ├── build.sh │ └── login.sh ├── container │ ├── entrypoint.sh │ ├── fetch_authorized_keys.sh │ └── Dockerfile └── main.tf ├── .gitignore ├── variables.tf ├── api-gateway-logger ├── variables.tf └── main.tf ├── outputs.tf ├── main.tf ├── README.md └── LICENSE /service/.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.1 2 | -------------------------------------------------------------------------------- /service/.gitignore: -------------------------------------------------------------------------------- 1 | .bastion-ip 2 | -------------------------------------------------------------------------------- /vpc/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" {} 2 | -------------------------------------------------------------------------------- /bastion/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" {} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.tfstate* 2 | .terraform 3 | plan 4 | -------------------------------------------------------------------------------- /service/api/lambda/create-bastion/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /service/api/lambda/destroy-bastion/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /service/create.rb: -------------------------------------------------------------------------------- 1 | require './session.rb' 2 | perform :create 3 | -------------------------------------------------------------------------------- /service/destroy.rb: -------------------------------------------------------------------------------- 1 | require './session.rb' 2 | perform :destroy 3 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" { 2 | default = "us-east-1" 3 | } 4 | -------------------------------------------------------------------------------- /service/outputs.tf: -------------------------------------------------------------------------------- 1 | output "endpoint" { 2 | value = module.api.invoke_url 3 | } 4 | -------------------------------------------------------------------------------- /api-gateway-logger/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" { 2 | default = "us-east-1" 3 | } 4 | -------------------------------------------------------------------------------- /service/api/outputs.tf: -------------------------------------------------------------------------------- 1 | output "invoke_url" { 2 | value = aws_api_gateway_deployment.bastion.invoke_url 3 | } 4 | -------------------------------------------------------------------------------- /service/api/lambda/trigger-bastion-destruction/resources/export.js: -------------------------------------------------------------------------------- 1 | 2 | exports.handle_request = trigger_bastion_destruction.handler.handle_request; 3 | -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | output "bastion_service_endpoint" { 2 | value = module.bastion_service.endpoint 3 | } 4 | 5 | output "region" { 6 | value = var.region 7 | } 8 | -------------------------------------------------------------------------------- /service/api/lambda/bastion/.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.jar 3 | /.lein-* 4 | /.nrepl-port 5 | /checkouts 6 | /classes 7 | /target 8 | pom.xml 9 | pom.xml.asc 10 | -------------------------------------------------------------------------------- /service/api/lambda/destroy-bastion/variables.tf: -------------------------------------------------------------------------------- 1 | variable "cluster_arn" {} 2 | variable "cluster_name" {} 3 | variable "cluster_vpc_default_security_group_id" {} 4 | variable "cluster_vpc_id" {} 5 | -------------------------------------------------------------------------------- /service/api/lambda/trigger-bastion-destruction/.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.jar 3 | /.lein-* 4 | /.nrepl-port 5 | /checkouts 6 | /classes 7 | /target 8 | node_modules 9 | pom.xml 10 | pom.xml.asc 11 | -------------------------------------------------------------------------------- /service/api/lambda/bastion/src/bastion/clients.clj: -------------------------------------------------------------------------------- 1 | (ns bastion.clients 2 | (:require [cognitect.aws.client.api :as aws])) 3 | 4 | (def ec2 (delay (aws/client {:api :ec2}))) 5 | (def ecs (delay (aws/client {:api :ecs}))) 6 | -------------------------------------------------------------------------------- /service/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 5 | 6 | gem 'aws-sdk-core' 7 | gem 'aws-sdk-s3' 8 | gem 'aws-sigv4' 9 | -------------------------------------------------------------------------------- /service/api/lambda/create-bastion/outputs.tf: -------------------------------------------------------------------------------- 1 | output "function_arn" { 2 | value = aws_lambda_function.create_bastion.arn 3 | } 4 | 5 | output "function_name" { 6 | value = aws_lambda_function.create_bastion.function_name 7 | } 8 | -------------------------------------------------------------------------------- /service/api/lambda/destroy-bastion/outputs.tf: -------------------------------------------------------------------------------- 1 | output "function_arn" { 2 | value = aws_lambda_function.destroy_bastion.arn 3 | } 4 | 5 | output "function_name" { 6 | value = aws_lambda_function.destroy_bastion.function_name 7 | } 8 | -------------------------------------------------------------------------------- /service/api/lambda/trigger-bastion-destruction/variables.tf: -------------------------------------------------------------------------------- 1 | variable "destroy_bastion_function_arn" {} 2 | variable "destroy_bastion_function_name" {} 3 | variable "http_method" {} 4 | variable "resource_path" {} 5 | variable "rest_api_id" {} 6 | -------------------------------------------------------------------------------- /service/api/lambda/trigger-bastion-destruction/outputs.tf: -------------------------------------------------------------------------------- 1 | output "function_arn" { 2 | value = aws_lambda_function.trigger_bastion_destruction.arn 3 | } 4 | 5 | output "function_name" { 6 | value = aws_lambda_function.trigger_bastion_destruction.function_name 7 | } 8 | -------------------------------------------------------------------------------- /vpc/outputs.tf: -------------------------------------------------------------------------------- 1 | output "public_subnet_ids" { 2 | value = [aws_subnet.public_az_a.id] 3 | } 4 | 5 | output "default_security_group_id" { 6 | value = aws_vpc.demo.default_security_group_id 7 | } 8 | 9 | output "vpc_id" { 10 | value = aws_vpc.demo.id 11 | } 12 | -------------------------------------------------------------------------------- /bastion/outputs.tf: -------------------------------------------------------------------------------- 1 | output "public_key_fetcher_role_arn" { 2 | value = aws_iam_role.public_key_fetcher.arn 3 | } 4 | 5 | output "repository_arn" { 6 | value = aws_ecr_repository.bastion.arn 7 | } 8 | 9 | output "repository_url" { 10 | value = aws_ecr_repository.bastion.repository_url 11 | } 12 | -------------------------------------------------------------------------------- /service/variables.tf: -------------------------------------------------------------------------------- 1 | variable "image_repository_arn" {} 2 | variable "image_repository_url" {} 3 | variable "public_key_fetcher_role_arn" {} 4 | 5 | variable "public_subnet_ids" { 6 | type = list(string) 7 | } 8 | 9 | variable "region" {} 10 | variable "task_role_policy_json" {} 11 | variable "vpc_default_security_group_id" {} 12 | variable "vpc_id" {} 13 | -------------------------------------------------------------------------------- /service/api/variables.tf: -------------------------------------------------------------------------------- 1 | variable "cluster_arn" {} 2 | variable "cluster_name" {} 3 | 4 | variable "cluster_subnet_ids" { 5 | type = list(string) 6 | } 7 | 8 | variable "cluster_vpc_default_security_group_id" {} 9 | variable "cluster_vpc_id" {} 10 | variable "container_name" {} 11 | variable "execution_role_arn" {} 12 | variable "task_family" {} 13 | variable "task_role_arn" {} 14 | -------------------------------------------------------------------------------- /service/api/lambda/trigger-bastion-destruction/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trigger-bastion-destruction", 3 | "version": "0.1.0-SNAPSHOT", 4 | "description": "trigger-bastion-destruction", 5 | "repository": "https://github.com/NuID/ethereum-registration/bastion/api/lambda/trigger-bastion-destruction", 6 | "private": true, 7 | "devDependencies": { 8 | "aws-sdk": "2.668.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /service/api/lambda/trigger-bastion-destruction/bin/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | lein clean && \ 4 | lein cljsbuild once && \ 5 | cat target/trigger-bastion-destruction/handler.js resources/export.js > target/trigger-bastion-destruction/index.js && \ 6 | mv target/trigger-bastion-destruction/index.js target/trigger-bastion-destruction/handler.js && \ 7 | cd target/trigger-bastion-destruction && \ 8 | zip -9qyr ../trigger-bastion-destruction.zip . 9 | -------------------------------------------------------------------------------- /bastion/bin/push.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)/../.." # Start from a consistent working directory 4 | 5 | echo "Fetching account ID..." 6 | account_id=$(aws sts get-caller-identity --query '[Account]' --output text) 7 | 8 | echo "Fetching bastion region..." 9 | region=$(terraform output region) 10 | 11 | echo "Pushing image..." 12 | docker push ${account_id}.dkr.ecr.${region}.amazonaws.com/bastion 13 | 14 | echo "Done" 15 | -------------------------------------------------------------------------------- /service/api/lambda/create-bastion/variables.tf: -------------------------------------------------------------------------------- 1 | variable "cluster_arn" {} 2 | variable "cluster_name" {} 3 | 4 | variable "cluster_subnet_ids" { 5 | type = list(string) 6 | } 7 | 8 | variable "cluster_vpc_default_security_group_id" {} 9 | variable "cluster_vpc_id" {} 10 | variable "container_name" {} 11 | variable "execution_role_arn" {} 12 | variable "http_method" {} 13 | variable "resource_path" {} 14 | variable "rest_api_id" {} 15 | variable "task_family" {} 16 | variable "task_role_arn" {} 17 | -------------------------------------------------------------------------------- /service/api/lambda/bastion/src/bastion/env.clj: -------------------------------------------------------------------------------- 1 | (ns bastion.env) 2 | 3 | (def cluster-name (System/getenv "CLUSTER_NAME")) 4 | (def cluster-subnet-ids (System/getenv "CLUSTER_SUBNET_IDS")) 5 | (def cluster-vpc-default-security-group-id (System/getenv "CLUSTER_VPC_DEFAULT_SECURITY_GROUP_ID")) 6 | (def cluster-vpc-id (System/getenv "CLUSTER_VPC_ID")) 7 | (def container-name (System/getenv "CONTAINER_NAME")) 8 | (def service-name (System/getenv "SERVICE_NAME")) 9 | (def task-family (System/getenv "TASK_FAMILY")) 10 | -------------------------------------------------------------------------------- /bastion/bin/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)/../.." # Start from a consistent working directory 4 | 5 | echo "Fetching account ID..." 6 | account_id=$(aws sts get-caller-identity --query '[Account]' --output text) 7 | 8 | echo "Fetching bastion region..." 9 | region=$(terraform output region) 10 | 11 | cd bastion 12 | 13 | echo "Building container..." 14 | docker build -t ${account_id}.dkr.ecr.${region}.amazonaws.com/bastion ./container 15 | -------------------------------------------------------------------------------- /bastion/bin/login.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)/../.." # Start from a consistent working directory 4 | 5 | echo "Fetching account ID..." 6 | account_id=$(aws sts get-caller-identity --query '[Account]' --output text) 7 | 8 | echo "Fetching bastion region..." 9 | region=$(terraform output region) 10 | 11 | echo "Logging into ECR..." 12 | aws ecr get-login-password \ 13 | --region ${region} \ 14 | | docker login \ 15 | --username AWS \ 16 | --password-stdin ${account_id}.dkr.ecr.${region}.amazonaws.com 17 | -------------------------------------------------------------------------------- /service/bin/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)/../.." # Start from a consistent working directory 4 | 5 | mkdir -p service/api/lambda/create-bastion/target 6 | mkdir -p service/api/lambda/destroy-bastion/target 7 | 8 | cd service/api/lambda/bastion 9 | lein with-profile create uberjar 10 | cp target/create-bastion.jar ../create-bastion/target/ 11 | 12 | lein with-profile destroy uberjar 13 | cp target/destroy-bastion.jar ../destroy-bastion/target/ 14 | 15 | cd ../trigger-bastion-destruction 16 | sh bin/build.sh 17 | -------------------------------------------------------------------------------- /service/api/lambda/bastion/src/bastion/destroy.clj: -------------------------------------------------------------------------------- 1 | (ns bastion.destroy 2 | (:gen-class 3 | :implements [com.amazonaws.services.lambda.runtime.RequestStreamHandler]) 4 | (:require [clojure.java.io :as io] 5 | [cheshire.core :as json] 6 | [bastion.sg :as sg] 7 | [bastion.task :as task])) 8 | 9 | (defn -handleRequest 10 | [_ input-stream _ _] 11 | (let [event (json/parse-stream (io/reader input-stream) true) 12 | user (:user event)] 13 | (if-let [task (task/get-for user)] 14 | (task/stop-for user task)) 15 | (sg/delete-for user))) 16 | -------------------------------------------------------------------------------- /service/bin/destroy-bastion.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)/../.." # Start from a consistent working directory 4 | 5 | if [[ -f "service/.bastion-ip" ]] 6 | then 7 | echo "Fetching bastion service endpoint..." 8 | invoke_url=$(terraform output bastion_service_endpoint) 9 | 10 | if [[ -z ${invoke_url} ]] 11 | then 12 | echo "No bastion service found." >&2 13 | exit 1 14 | fi 15 | 16 | cd service 17 | 18 | echo "Destroying bastion..." 19 | INVOKE_URL=${invoke_url} bundle exec ruby destroy.rb 20 | 21 | rm .bastion-ip 22 | fi 23 | 24 | echo "Done" 25 | -------------------------------------------------------------------------------- /service/container-definitions.tpl.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "${name}", 4 | "image": "${image}", 5 | "portMappings": [ 6 | { 7 | "containerPort": 22 8 | } 9 | ], 10 | "environment": [ 11 | { 12 | "name": "ASSUME_ROLE_FOR_AUTHORIZED_KEYS", 13 | "value": "${assume_role_for_authorized_keys}" 14 | } 15 | ], 16 | "logConfiguration": { 17 | "logDriver": "awslogs", 18 | "options": { 19 | "awslogs-group": "${log_group_name}", 20 | "awslogs-region": "${region}", 21 | "awslogs-stream-prefix": "ssh" 22 | } 23 | } 24 | } 25 | ] 26 | -------------------------------------------------------------------------------- /service/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | aws-eventstream (1.1.0) 5 | aws-partitions (1.308.0) 6 | aws-sdk-core (3.94.0) 7 | aws-eventstream (~> 1, >= 1.0.2) 8 | aws-partitions (~> 1, >= 1.239.0) 9 | aws-sigv4 (~> 1.1) 10 | jmespath (~> 1.0) 11 | aws-sdk-kms (1.30.0) 12 | aws-sdk-core (~> 3, >= 3.71.0) 13 | aws-sigv4 (~> 1.1) 14 | aws-sdk-s3 (1.63.0) 15 | aws-sdk-core (~> 3, >= 3.83.0) 16 | aws-sdk-kms (~> 1) 17 | aws-sigv4 (~> 1.1) 18 | aws-sigv4 (1.1.3) 19 | aws-eventstream (~> 1.0, >= 1.0.2) 20 | jmespath (1.4.0) 21 | 22 | PLATFORMS 23 | ruby 24 | 25 | DEPENDENCIES 26 | aws-sdk-core 27 | aws-sdk-s3 28 | aws-sigv4 29 | 30 | BUNDLED WITH 31 | 2.1.4 32 | -------------------------------------------------------------------------------- /service/bin/create-bastion.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)/../.." # Start from a consistent working directory 4 | 5 | echo "Fetching bastion service endpoint..." 6 | invoke_url=$(terraform output bastion_service_endpoint) 7 | 8 | if [[ -z ${invoke_url} ]] 9 | then 10 | echo "No bastion service found." >&2 11 | exit 1 12 | fi 13 | 14 | cd service 15 | 16 | echo "Creating bastion..." 17 | INVOKE_URL=${invoke_url} bundle exec ruby create.rb | jq -r .ip > .bastion-ip 18 | 19 | if [[ -z $(cat .bastion-ip) ]] 20 | then 21 | rm .bastion-ip 22 | echo "No IP address returned. Probably just AWS being slow. Try re-running this script." >&2 23 | exit 1 24 | else 25 | echo "Done" 26 | echo "ops@$(cat .bastion-ip)" 27 | fi 28 | -------------------------------------------------------------------------------- /service/api/main.tf: -------------------------------------------------------------------------------- 1 | data "aws_region" "current" {} 2 | 3 | resource "aws_api_gateway_rest_api" "bastion" { 4 | name = "Bastion Management" 5 | } 6 | 7 | resource "aws_api_gateway_deployment" "bastion" { 8 | depends_on = [ 9 | aws_api_gateway_integration.delete_bastion, 10 | aws_api_gateway_integration.post_bastion, 11 | ] 12 | 13 | rest_api_id = aws_api_gateway_rest_api.bastion.id 14 | stage_name = "demo" 15 | } 16 | 17 | resource "aws_api_gateway_method_settings" "bastion" { 18 | method_path = "*/*" 19 | rest_api_id = aws_api_gateway_rest_api.bastion.id 20 | 21 | settings { 22 | metrics_enabled = true 23 | logging_level = "INFO" 24 | data_trace_enabled = true 25 | } 26 | 27 | stage_name = aws_api_gateway_deployment.bastion.stage_name 28 | } 29 | -------------------------------------------------------------------------------- /service/api/lambda/bastion/src/bastion/util.clj: -------------------------------------------------------------------------------- 1 | (ns bastion.util 2 | (:require [clojure.string :as string] 3 | [digest] 4 | [bastion.env :as env])) 5 | 6 | (def ^:const ^:private started-by-max-length 36) ; Per https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_RunTask.html 7 | 8 | (defn user-hash 9 | [user] 10 | (let [hash (digest/sha-256 user)] 11 | (subs hash 0 (- started-by-max-length 1)))) 12 | 13 | (defn attachment-description 14 | [task] 15 | (let [task-arn (:taskArn task) 16 | attachment-id (:id (first (:attachments task))) 17 | identifier (str "attachment/" attachment-id) 18 | attachment-description (string/replace task-arn #"task/.*" identifier)] 19 | attachment-description)) 20 | 21 | (defn security-group-name 22 | [user] 23 | (str env/cluster-name "/" user)) 24 | -------------------------------------------------------------------------------- /service/api/lambda/trigger-bastion-destruction/project.clj: -------------------------------------------------------------------------------- 1 | (defproject trigger-bastion-destruction "0.1.0-SNAPSHOT" 2 | :description "trigger-bastion-destruction" 3 | :dependencies [[org.clojure/clojure "1.10.1"] 4 | [org.clojure/clojurescript "1.10.753"]] 5 | :plugins [[lein-cljsbuild "1.1.8"]] 6 | :cljsbuild {:builds [{:source-paths ["src"] 7 | :compiler {:output-to "target/trigger-bastion-destruction/handler.js" 8 | :output-dir "target/trigger-bastion-destruction" 9 | :asset-path "" 10 | :source-map false 11 | :target :nodejs 12 | :main "trigger-bastion-destruction.handler" 13 | :optimizations :none 14 | :parallel-build true}}]}) 15 | -------------------------------------------------------------------------------- /service/api/lambda/trigger-bastion-destruction/src/trigger_bastion_destruction/handler.cljs: -------------------------------------------------------------------------------- 1 | (ns trigger-bastion-destruction.handler 2 | (:require [clojure.string :as cs] 3 | [cljs.nodejs :as node])) 4 | 5 | (node/enable-util-print!) 6 | 7 | (def AWS 8 | (node/require "aws-sdk")) 9 | 10 | (def lambda 11 | (new AWS.Lambda)) 12 | 13 | (defn env 14 | [k] 15 | (aget js/process.env k)) 16 | 17 | (def destroy-bastion-function-name (env "DESTROY_BASTION_FUNCTION_NAME")) 18 | 19 | (defn ^:export handle-request 20 | [event _ callback] 21 | (let [event (js->clj event :keywordize-keys true) 22 | user (last (cs/split (get-in event [:requestContext :identity :userArn]) #"/")) 23 | payload (.stringify js/JSON (clj->js {:user user}))] 24 | (.invoke lambda 25 | (clj->js {:FunctionName destroy-bastion-function-name 26 | :InvocationType "Event" 27 | :Payload payload}) 28 | (fn [_ _] 29 | (callback nil (clj->js {:statusCode 200})))))) 30 | -------------------------------------------------------------------------------- /bastion/container/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | set -u 5 | 6 | echo "Creating host keys..." 7 | ssh-keygen -A 8 | 9 | echo "Exporting global environment..." 10 | 11 | echo "export AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" > /etc/profile.d/authorized_keys_configuration.sh 12 | echo "export AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION" >> /etc/profile.d/authorized_keys_configuration.sh 13 | echo "export AWS_EXECUTION_ENV=$AWS_EXECUTION_ENV" >> /etc/profile.d/authorized_keys_configuration.sh 14 | echo "export AWS_REGION=$AWS_REGION" >> /etc/profile.d/authorized_keys_configuration.sh 15 | echo "export ECS_CONTAINER_METADATA_URI=$ECS_CONTAINER_METADATA_URI" >> /etc/profile.d/authorized_keys_configuration.sh 16 | echo "export ASSUME_ROLE_FOR_AUTHORIZED_KEYS=$ASSUME_ROLE_FOR_AUTHORIZED_KEYS" >> /etc/profile.d/authorized_keys_configuration.sh 17 | echo "export USER_NAME=$USER_NAME" >> /etc/profile.d/authorized_keys_configuration.sh 18 | 19 | chmod +x /etc/profile.d/authorized_keys_configuration.sh 20 | 21 | exec /usr/sbin/sshd -D -e "$@" 22 | -------------------------------------------------------------------------------- /bastion/main.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "env" {} 2 | 3 | resource "aws_ecr_repository" "bastion" { 4 | name = "bastion" 5 | } 6 | 7 | data "aws_iam_policy_document" "assume_role" { 8 | statement { 9 | actions = ["sts:AssumeRole"] 10 | 11 | principals { 12 | identifiers = ["arn:aws:iam::${data.aws_caller_identity.env.account_id}:root"] 13 | type = "AWS" 14 | } 15 | } 16 | } 17 | 18 | data "aws_iam_policy_document" "public_key_fetcher" { 19 | statement { 20 | actions = [ 21 | "iam:GetSSHPublicKey", 22 | "iam:ListSSHPublicKeys", 23 | ] 24 | 25 | resources = ["arn:aws:iam::${data.aws_caller_identity.env.account_id}:user/*"] 26 | } 27 | } 28 | 29 | resource "aws_iam_role" "public_key_fetcher" { 30 | name = "public-key-fetcher" 31 | assume_role_policy = data.aws_iam_policy_document.assume_role.json 32 | } 33 | 34 | resource "aws_iam_role_policy" "public_key_fetcher" { 35 | name = "public-key-fetcher" 36 | policy = data.aws_iam_policy_document.public_key_fetcher.json 37 | role = aws_iam_role.public_key_fetcher.id 38 | } 39 | -------------------------------------------------------------------------------- /bastion/container/fetch_authorized_keys.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -u 5 | 6 | source /etc/profile.d/authorized_keys_configuration.sh 7 | 8 | sts_credentials=$(/usr/local/bin/aws sts assume-role \ 9 | --role-arn "${ASSUME_ROLE_FOR_AUTHORIZED_KEYS}" \ 10 | --role-session-name fetch-authorized-keys-for-bastion \ 11 | --query '[Credentials.SessionToken,Credentials.AccessKeyId,Credentials.SecretAccessKey]' \ 12 | --output text) 13 | 14 | AWS_ACCESS_KEY_ID=$(echo "${sts_credentials}" | awk '{print $2}') 15 | AWS_SECRET_ACCESS_KEY=$(echo "${sts_credentials}" | awk '{print $3}') 16 | AWS_SESSION_TOKEN=$(echo "${sts_credentials}" | awk '{print $1}') 17 | AWS_SECURITY_TOKEN=$(echo "${sts_credentials}" | awk '{print $1}') 18 | export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_SECURITY_TOKEN 19 | 20 | /usr/local/bin/aws iam list-ssh-public-keys --user-name "$USER_NAME" --query "SSHPublicKeys[?Status == 'Active'].[SSHPublicKeyId]" --output text | while read -r key_id; do 21 | /usr/local/bin/aws iam get-ssh-public-key --user-name "$USER_NAME" --ssh-public-key-id "$key_id" --encoding SSH --query "SSHPublicKey.SSHPublicKeyBody" --output text 22 | done 23 | -------------------------------------------------------------------------------- /service/api/lambda/bastion/project.clj: -------------------------------------------------------------------------------- 1 | (defproject bastion "0.1.0-SNAPSHOT" 2 | :description "bastion" 3 | :dependencies [[org.clojure/clojure "1.10.1"] 4 | [org.clojure/core.async "1.1.587"] 5 | [com.cognitect.aws/api "0.8.456"] 6 | [com.cognitect.aws/endpoints "1.1.11.774"] 7 | [com.cognitect.aws/ec2 "796.2.657.0"] 8 | [com.cognitect.aws/ecs "796.2.656.0"] 9 | [com.amazonaws/aws-lambda-java-core "1.2.1"] 10 | [cheshire "5.10.0"] 11 | [digest "1.4.9"]] 12 | :jvm-opts ["-Dclojure.compiler.elide-meta=[:doc]" 13 | "-Dclojure.compiler.direct-linking=true"] 14 | :profiles {:uberjar {:aot :all 15 | :global-vars {*warn-on-reflection* true}} 16 | :create {:main bastion.create 17 | :name "create-bastion" 18 | :uberjar-name "create-bastion.jar"} 19 | :destroy {:main bastion.destroy 20 | :name "destroy-bastion" 21 | :uberjar-name "destroy-bastion.jar"}}) 22 | -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 0.12.0" 3 | } 4 | 5 | provider "aws" { 6 | version = "~> 2.0" 7 | region = var.region 8 | } 9 | 10 | module "bastion" { 11 | source = "./bastion" 12 | region = var.region 13 | } 14 | 15 | module "vpc" { 16 | source = "./vpc" 17 | region = var.region 18 | } 19 | 20 | data "aws_iam_policy_document" "bastion_task_role" { 21 | statement { 22 | actions = ["sts:AssumeRole"] 23 | resources = [module.bastion.public_key_fetcher_role_arn] 24 | } 25 | 26 | # 27 | # Add any other permissions needed here 28 | # 29 | } 30 | 31 | module "bastion_service" { 32 | source = "./service" 33 | 34 | image_repository_arn = module.bastion.repository_arn 35 | image_repository_url = module.bastion.repository_url 36 | public_key_fetcher_role_arn = module.bastion.public_key_fetcher_role_arn 37 | public_subnet_ids = module.vpc.public_subnet_ids 38 | region = var.region 39 | task_role_policy_json = data.aws_iam_policy_document.bastion_task_role.json 40 | vpc_default_security_group_id = module.vpc.default_security_group_id 41 | vpc_id = module.vpc.vpc_id 42 | } 43 | -------------------------------------------------------------------------------- /api-gateway-logger/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 0.12.0" 3 | } 4 | 5 | provider "aws" { 6 | version = "~> 2.0" 7 | region = var.region 8 | } 9 | 10 | data "aws_iam_policy_document" "assume_role" { 11 | statement { 12 | actions = ["sts:AssumeRole"] 13 | 14 | principals { 15 | identifiers = ["apigateway.amazonaws.com"] 16 | type = "Service" 17 | } 18 | } 19 | } 20 | 21 | data "aws_iam_policy_document" "logger" { 22 | statement { 23 | actions = [ 24 | "logs:CreateLogGroup", 25 | "logs:CreateLogStream", 26 | "logs:DescribeLogGroups", 27 | "logs:DescribeLogStreams", 28 | "logs:FilterLogEvents", 29 | "logs:GetLogEvents", 30 | "logs:PutLogEvents", 31 | ] 32 | 33 | resources = ["*"] 34 | } 35 | } 36 | 37 | resource "aws_iam_role" "logger" { 38 | name = "api-gateway-cloudwatch-logger" 39 | assume_role_policy = data.aws_iam_policy_document.assume_role.json 40 | } 41 | 42 | resource "aws_iam_role_policy" "logger" { 43 | name = "api-gateway-cloudwatch-logger" 44 | policy = data.aws_iam_policy_document.logger.json 45 | role = aws_iam_role.logger.name 46 | } 47 | 48 | resource "aws_api_gateway_account" "global" { 49 | cloudwatch_role_arn = aws_iam_role.logger.arn 50 | } 51 | -------------------------------------------------------------------------------- /service/session.rb: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler.require 3 | 4 | require 'json' 5 | require 'net/http' 6 | require 'uri' 7 | 8 | INVOKE_URL = ENV['INVOKE_URL'] 9 | REGION = URI.parse(INVOKE_URL).host.split('.')[2] 10 | 11 | BASE_HEADERS = { 12 | 'Content-Type' => 'application/json', 13 | 'Accept' => 'application/json' 14 | } 15 | 16 | def perform(action) 17 | uri = URI.parse("#{INVOKE_URL}/bastion") 18 | 19 | method = case action 20 | when :create 21 | 'POST' 22 | when :destroy 23 | 'DELETE' 24 | end 25 | 26 | signer = Aws::Sigv4::Signer.new( 27 | service: 'execute-api', 28 | region: REGION, 29 | credentials_provider: Aws::SharedCredentials.new 30 | ) 31 | 32 | signature = signer.sign_request( 33 | http_method: method, 34 | url: uri.to_s, 35 | headers: BASE_HEADERS 36 | ) 37 | 38 | http = Net::HTTP.new(uri.host, uri.port) 39 | http.use_ssl = true 40 | 41 | action_request = case method 42 | when 'POST' 43 | Net::HTTP::Post.new(uri) 44 | when 'DELETE' 45 | Net::HTTP::Delete.new(uri) 46 | end 47 | 48 | BASE_HEADERS.merge(signature.headers).each { |k, v| 49 | action_request[k] = v 50 | } 51 | 52 | action_request['accept-encoding'] = nil 53 | action_request['user-agent'] = nil 54 | 55 | response = http.request(action_request) 56 | 57 | if response.code == '201' 58 | puts "#{JSON.pretty_generate(JSON.parse(response.body))}" 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /service/api/lambda/bastion/src/bastion/eni.clj: -------------------------------------------------------------------------------- 1 | (ns bastion.eni 2 | (:require [clojure.core.async :refer [ (count network-interfaces) 0) 21 | (do 22 | (> (sg/create-for user cidr-ip) 25 | (task/run-for user)))] 26 | (stream-response (:bastion-ip task) output-stream))) 27 | 28 | (defn -handleRequest 29 | [_ input-stream output-stream _] 30 | (let [event (json/parse-stream (io/reader input-stream) true) 31 | cidr-ip (str (get-in event [:requestContext :identity :sourceIp]) "/32") 32 | user (last (cs/split (get-in event [:requestContext :identity :userArn]) #"/"))] 33 | (if-let [security-group-id (sg/get-id-for user)] 34 | (if (sg/ip-matches? security-group-id cidr-ip) 35 | (if-let [task (task/get-for user)] 36 | (stream-response (:bastion-ip task) output-stream) 37 | (start-bastion-and-stream-response user cidr-ip output-stream security-group-id)) 38 | (if-let [task (task/get-for user)] 39 | (do 40 | (task/stop-for user task) 41 | (sg/delete-for user) 42 | (start-bastion-and-stream-response user cidr-ip output-stream)) 43 | (do 44 | (sg/delete-for user) 45 | (start-bastion-and-stream-response user cidr-ip output-stream)))) 46 | (start-bastion-and-stream-response user cidr-ip output-stream)))) 47 | -------------------------------------------------------------------------------- /service/api/lambda/trigger-bastion-destruction/main.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current" {} 2 | data "aws_region" "current" {} 3 | 4 | data "aws_iam_policy_document" "assume_role" { 5 | statement { 6 | actions = ["sts:AssumeRole"] 7 | 8 | principals { 9 | identifiers = ["lambda.amazonaws.com"] 10 | type = "Service" 11 | } 12 | } 13 | } 14 | 15 | data "aws_iam_policy_document" "trigger_bastion_destruction" { 16 | statement { 17 | actions = [ 18 | "logs:CreateLogGroup", 19 | "logs:CreateLogStream", 20 | "logs:PutLogEvents", 21 | ] 22 | 23 | resources = [ 24 | "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:/aws/lambda/trigger-bastion-destruction", 25 | "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:/aws/lambda/trigger-bastion-destruction:*", 26 | ] 27 | } 28 | 29 | statement { 30 | actions = ["lambda:InvokeFunction"] 31 | resources = [var.destroy_bastion_function_arn] 32 | } 33 | } 34 | 35 | resource "aws_iam_role" "trigger_bastion_destruction" { 36 | name = "trigger-bastion-destruction" 37 | assume_role_policy = data.aws_iam_policy_document.assume_role.json 38 | } 39 | 40 | resource "aws_iam_role_policy" "trigger_bastion_destruction" { 41 | name = "trigger-bastion-destruction" 42 | policy = data.aws_iam_policy_document.trigger_bastion_destruction.json 43 | role = aws_iam_role.trigger_bastion_destruction.name 44 | } 45 | 46 | resource "aws_lambda_function" "trigger_bastion_destruction" { 47 | filename = "${path.module}/target/trigger-bastion-destruction.zip" 48 | function_name = "trigger-bastion-destruction" 49 | handler = "handler.handle_request" 50 | memory_size = 512 51 | publish = true 52 | role = aws_iam_role.trigger_bastion_destruction.arn 53 | runtime = "nodejs12.x" 54 | source_code_hash = filebase64sha256("${path.module}/target/trigger-bastion-destruction.zip") 55 | 56 | environment { 57 | variables = { 58 | DESTROY_BASTION_FUNCTION_NAME = var.destroy_bastion_function_name 59 | } 60 | } 61 | } 62 | 63 | resource "aws_lambda_permission" "api_gateway" { 64 | statement_id = "AllowExecutionFromAPIGateway" 65 | action = "lambda:InvokeFunction" 66 | function_name = aws_lambda_function.trigger_bastion_destruction.arn 67 | principal = "apigateway.amazonaws.com" 68 | source_arn = "arn:aws:execute-api:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${var.rest_api_id}/*/${var.http_method}${var.resource_path}" 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bastions on Demand 2 | 3 | This is a fully functional example of how to create and destroy bastion instances on demand using [Fargate](https://aws.amazon.com/fargate/). 4 | 5 | For an in-depth guide to this example, check out ["Bastions on Demand"](https://theconsultingcto.com/posts/bastions-on-demand) on my site. 6 | 7 | ## Preliminaries 8 | 9 | Before you being, you will need to install: 10 | 11 | - [AWS CLI](https://aws.amazon.com/cli/) 12 | - [Bundler](https://bundler.io) 13 | - [Docker](https://www.docker.com) 14 | - [jq](https://stedolan.github.io/jq/) 15 | - [Leiningen](https://leiningen.org) 16 | - [Terraform](https://www.terraform.io) 17 | 18 | Everything in this repo assumes use of the `default` AWS profile. You can easily override that assumption with the `AWS_PROFILE` environment variable. 19 | 20 | You can configure your credentials with [`aws configure`](https://docs.aws.amazon.com/cli/latest/reference/configure/). 21 | 22 | You will also need to upload your public SSH key to your IAM user using either the AWS Console or the CLI (if you haven't already). 23 | 24 | ## Setup 25 | 26 | If you haven't previously configured [a CloudWatch role for API Gateway](https://aws.amazon.com/premiumsupport/knowledge-center/api-gateway-cloudwatch-logs/), then use the [`api-gateway-logger`](https://github.com/jdhollis/bastions-on-demand/tree/master/api-gateway-logger) module to do so now: 27 | 28 | ```bash 29 | cd api-gateway-logger 30 | terraform init 31 | terraform plan -out plan 32 | terraform apply plan && rm plan 33 | cd .. 34 | ``` 35 | 36 | This is a global account setting, so you should only have to do it once. Note that destroying the module's resources with Terraform will remove the role, but [it will not reset the CloudWatch role setting for API Gateway](https://www.terraform.io/docs/providers/aws/r/api_gateway_account.html). 37 | 38 | Now we're ready to create the service. 39 | 40 | ```bash 41 | terraform init 42 | ./service/bin/build.sh # Build the Lambda functions 43 | terraform apply plan && rm plan 44 | ``` 45 | 46 | Once the Terraform successfully applies, fire up Docker (if you don't already have it running). Then, build and push the bastion image with: 47 | 48 | ```bash 49 | ./bastion/bin/login.sh # Log into ECR 50 | ./bastion/bin/build.sh # Build & tag the Docker image 51 | ./bastion/bin/push.sh # Push the tagged image to ECR 52 | ``` 53 | 54 | Finally, we need to make certain the necessary Ruby dependencies are installed: 55 | 56 | ```bash 57 | cd service 58 | bundle 59 | cd .. 60 | ``` 61 | 62 | You should now be able to create and destroy bastions with: 63 | 64 | ```bash 65 | ./service/bin/create-bastion.sh 66 | ./service/bin/destroy-bastion.sh 67 | ``` 68 | 69 | Once a bastion is running, you'll find its IP address in `service/.bastion-ip`. 70 | 71 | You can `ssh` into the bastion with: 72 | 73 | ```bash 74 | ssh ops@$(cat service/.bastion-ip) 75 | ``` 76 | -------------------------------------------------------------------------------- /service/api/lambda/destroy-bastion/main.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current" {} 2 | data "aws_region" "current" {} 3 | 4 | data "aws_iam_policy_document" "assume_role" { 5 | statement { 6 | actions = ["sts:AssumeRole"] 7 | 8 | principals { 9 | identifiers = ["lambda.amazonaws.com"] 10 | type = "Service" 11 | } 12 | } 13 | } 14 | 15 | resource "aws_sqs_queue" "dlq" { 16 | name = "destroy-bastion-dlq" 17 | } 18 | 19 | data "aws_iam_policy_document" "destroy_bastion" { 20 | statement { 21 | actions = [ 22 | "logs:CreateLogGroup", 23 | "logs:CreateLogStream", 24 | "logs:PutLogEvents", 25 | ] 26 | 27 | resources = [ 28 | "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:/aws/lambda/destroy-bastion", 29 | "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:/aws/lambda/destroy-bastion:*", 30 | ] 31 | } 32 | 33 | statement { 34 | actions = ["sqs:SendMessage"] 35 | resources = [aws_sqs_queue.dlq.arn] 36 | } 37 | 38 | statement { 39 | actions = [ 40 | "ec2:DeleteSecurityGroup", 41 | "ec2:DescribeNetworkInterfaces", 42 | "ec2:DescribeSecurityGroups", 43 | "ec2:RevokeSecurityGroupIngress", 44 | ] 45 | 46 | resources = ["*"] 47 | } 48 | 49 | statement { 50 | actions = [ 51 | "ecs:DescribeTask*", 52 | "ecs:ListTask*", 53 | "ecs:StopTask", 54 | ] 55 | 56 | resources = ["*"] 57 | 58 | condition { 59 | test = "ArnEquals" 60 | values = [var.cluster_arn] 61 | variable = "ecs:cluster" 62 | } 63 | } 64 | } 65 | 66 | resource "aws_iam_role" "destroy_bastion" { 67 | name = "destroy-bastion" 68 | assume_role_policy = data.aws_iam_policy_document.assume_role.json 69 | } 70 | 71 | resource "aws_iam_role_policy" "destroy_bastion" { 72 | name = "destroy-bastion" 73 | policy = data.aws_iam_policy_document.destroy_bastion.json 74 | role = aws_iam_role.destroy_bastion.name 75 | } 76 | 77 | resource "aws_lambda_function" "destroy_bastion" { 78 | filename = "${path.module}/target/destroy-bastion.jar" 79 | function_name = "destroy-bastion" 80 | handler = "bastion.destroy" 81 | memory_size = 512 82 | publish = true 83 | role = aws_iam_role.destroy_bastion.arn 84 | runtime = "java8" 85 | source_code_hash = filebase64sha256("${path.module}/target/destroy-bastion.jar") 86 | timeout = 120 87 | 88 | dead_letter_config { 89 | target_arn = aws_sqs_queue.dlq.arn 90 | } 91 | 92 | environment { 93 | variables = { 94 | CLUSTER_NAME = var.cluster_name 95 | CLUSTER_VPC_DEFAULT_SECURITY_GROUP_ID = var.cluster_vpc_default_security_group_id 96 | CLUSTER_VPC_ID = var.cluster_vpc_id 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /service/api/lambda/bastion/src/bastion/task.clj: -------------------------------------------------------------------------------- 1 | (ns bastion.task 2 | (:require [clojure.string :as string] 3 | [cognitect.aws.client.api :as aws] 4 | [bastion.clients :as clients] 5 | [bastion.eni :as eni] 6 | [bastion.env :as env] 7 | [bastion.util :as util])) 8 | 9 | (defn- bastion-task 10 | [task] 11 | (let [attachment-description (util/attachment-description task)] 12 | {:task-arn (:taskArn task) 13 | :attachment-description attachment-description 14 | :bastion-ip (eni/get-public-ip attachment-description)})) 15 | 16 | (defn get-for 17 | [user] 18 | (println "Getting running bastion for" user) 19 | (let [list-tasks (aws/invoke @clients/ecs 20 | {:op :ListTasks 21 | :request {:cluster env/cluster-name 22 | :startedBy (util/user-hash user)}}) 23 | task-arns (:taskArns list-tasks)] 24 | (if (> (count task-arns) 0) 25 | (if-let [describe-tasks (aws/invoke @clients/ecs 26 | {:op :DescribeTasks 27 | :request {:cluster env/cluster-name 28 | :tasks task-arns}})] 29 | (let [task (first (:tasks describe-tasks))] 30 | (bastion-task task)) 31 | nil)))) 32 | 33 | (defn run-for 34 | [user security-group-id] 35 | (println "Running bastion for" user) 36 | (let [response (aws/invoke @clients/ecs 37 | {:op :RunTask 38 | :request {:cluster env/cluster-name 39 | :taskDefinition env/task-family 40 | :count 1 41 | :startedBy (util/user-hash user) 42 | :launchType "FARGATE" 43 | :networkConfiguration {:awsvpcConfiguration 44 | {:subnets (string/split env/cluster-subnet-ids #",") 45 | :securityGroups [security-group-id] 46 | :assignPublicIp "ENABLED"}} 47 | :overrides {:containerOverrides 48 | [{:name env/container-name 49 | :environment [{:name "USER_NAME" 50 | :value user}]}]}}}) 51 | task (first (:tasks response))] 52 | (bastion-task task))) 53 | 54 | (defn stop-for 55 | [user task] 56 | (println "Stopping bastion for" user) 57 | (aws/invoke @clients/ecs 58 | {:op :StopTask 59 | :request {:cluster env/cluster-name 60 | :task (:task-arn task) 61 | :reason "Requested by user"}}) 62 | (eni/wait-for-deletion (:attachment-description task))) 63 | -------------------------------------------------------------------------------- /service/main.tf: -------------------------------------------------------------------------------- 1 | provider "template" { 2 | version = "~> 2.1" 3 | } 4 | 5 | resource "aws_cloudwatch_log_group" "bastion" { 6 | name = "bastion" 7 | } 8 | 9 | data "aws_iam_policy_document" "assume_role" { 10 | statement { 11 | actions = ["sts:AssumeRole"] 12 | 13 | principals { 14 | identifiers = ["ecs-tasks.amazonaws.com"] 15 | type = "Service" 16 | } 17 | } 18 | } 19 | 20 | resource "aws_iam_role" "execution_role" { 21 | name = "bastion-execution" 22 | assume_role_policy = data.aws_iam_policy_document.assume_role.json 23 | } 24 | 25 | data "aws_iam_policy_document" "execution_role" { 26 | statement { 27 | actions = ["ecr:GetAuthorizationToken"] 28 | resources = ["*"] 29 | } 30 | 31 | statement { 32 | actions = [ 33 | "ecr:BatchCheckLayerAvailability", 34 | "ecr:BatchGetImage", 35 | "ecr:GetDownloadUrlForLayer", 36 | ] 37 | 38 | resources = [var.image_repository_arn] 39 | } 40 | 41 | statement { 42 | actions = [ 43 | "logs:CreateLogStream", 44 | "logs:PutLogEvents", 45 | ] 46 | 47 | resources = ["*"] 48 | } 49 | } 50 | 51 | resource "aws_iam_role_policy" "execution_role" { 52 | name = "bastion-execution" 53 | policy = data.aws_iam_policy_document.execution_role.json 54 | role = aws_iam_role.execution_role.id 55 | } 56 | 57 | resource "aws_iam_role" "task_role" { 58 | name = "bastion-task" 59 | assume_role_policy = data.aws_iam_policy_document.assume_role.json 60 | } 61 | 62 | resource "aws_iam_role_policy" "task_role" { 63 | policy = var.task_role_policy_json 64 | role = aws_iam_role.task_role.id 65 | } 66 | 67 | resource "aws_ecs_cluster" "bastion" { 68 | name = "bastions" 69 | } 70 | 71 | data "template_file" "container_definitions" { 72 | template = file("${path.module}/container-definitions.tpl.json") 73 | 74 | vars = { 75 | assume_role_for_authorized_keys = var.public_key_fetcher_role_arn 76 | image = "${var.image_repository_url}:latest" 77 | log_group_name = aws_cloudwatch_log_group.bastion.name 78 | name = "bastion" 79 | region = var.region 80 | } 81 | } 82 | 83 | resource "aws_ecs_task_definition" "bastion" { 84 | container_definitions = data.template_file.container_definitions.rendered 85 | cpu = "256" 86 | execution_role_arn = aws_iam_role.execution_role.arn 87 | family = "bastions" 88 | memory = "512" 89 | network_mode = "awsvpc" 90 | requires_compatibilities = ["FARGATE"] 91 | task_role_arn = aws_iam_role.task_role.arn 92 | } 93 | 94 | module "api" { 95 | source = "./api" 96 | 97 | cluster_arn = aws_ecs_cluster.bastion.arn 98 | cluster_name = aws_ecs_cluster.bastion.name 99 | cluster_subnet_ids = var.public_subnet_ids 100 | cluster_vpc_default_security_group_id = var.vpc_default_security_group_id 101 | cluster_vpc_id = var.vpc_id 102 | container_name = "bastion" 103 | execution_role_arn = aws_iam_role.execution_role.arn 104 | task_family = aws_ecs_task_definition.bastion.family 105 | task_role_arn = aws_iam_role.task_role.arn 106 | } 107 | -------------------------------------------------------------------------------- /service/api/lambda/create-bastion/main.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current" {} 2 | data "aws_region" "current" {} 3 | 4 | data "aws_iam_policy_document" "assume_role" { 5 | statement { 6 | actions = ["sts:AssumeRole"] 7 | 8 | principals { 9 | identifiers = ["lambda.amazonaws.com"] 10 | type = "Service" 11 | } 12 | } 13 | } 14 | 15 | data "aws_iam_policy_document" "create_bastion" { 16 | statement { 17 | actions = [ 18 | "logs:CreateLogGroup", 19 | "logs:CreateLogStream", 20 | "logs:PutLogEvents", 21 | ] 22 | 23 | resources = [ 24 | "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:/aws/lambda/create-bastion$", 25 | "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:/aws/lambda/create-bastion:*", 26 | ] 27 | } 28 | 29 | statement { 30 | actions = [ 31 | "ec2:AuthorizeSecurityGroupIngress", 32 | "ec2:CreateSecurityGroup", 33 | "ec2:DeleteSecurityGroup", 34 | "ec2:DescribeNetworkInterfaces", 35 | "ec2:DescribeSecurityGroups", 36 | ] 37 | 38 | resources = ["*"] 39 | } 40 | 41 | statement { 42 | actions = [ 43 | "ecs:DescribeTask*", 44 | "ecs:ListTask*", 45 | "ecs:RunTask", 46 | "ecs:StopTask", 47 | ] 48 | 49 | resources = ["*"] 50 | 51 | condition { 52 | test = "ArnEquals" 53 | values = [var.cluster_arn] 54 | variable = "ecs:cluster" 55 | } 56 | } 57 | 58 | statement { 59 | actions = ["iam:PassRole"] 60 | 61 | resources = [ 62 | var.execution_role_arn, 63 | var.task_role_arn, 64 | ] 65 | 66 | condition { 67 | test = "StringLike" 68 | values = ["ecs-tasks.amazonaws.com"] 69 | variable = "iam:PassedToService" 70 | } 71 | } 72 | } 73 | 74 | resource "aws_iam_role" "create_bastion" { 75 | name = "create-bastion" 76 | assume_role_policy = data.aws_iam_policy_document.assume_role.json 77 | } 78 | 79 | resource "aws_iam_role_policy" "create_bastion" { 80 | name = "create-bastion" 81 | policy = data.aws_iam_policy_document.create_bastion.json 82 | role = aws_iam_role.create_bastion.name 83 | } 84 | 85 | resource "aws_lambda_function" "create_bastion" { 86 | filename = "${path.module}/target/create-bastion.jar" 87 | function_name = "create-bastion" 88 | handler = "bastion.create" 89 | memory_size = 512 90 | publish = true 91 | role = aws_iam_role.create_bastion.arn 92 | runtime = "java8" 93 | source_code_hash = filebase64sha256("${path.module}/target/create-bastion.jar") 94 | timeout = 120 95 | 96 | environment { 97 | variables = { 98 | CLUSTER_NAME = var.cluster_name 99 | CLUSTER_SUBNET_IDS = join(",", var.cluster_subnet_ids) 100 | CLUSTER_VPC_DEFAULT_SECURITY_GROUP_ID = var.cluster_vpc_default_security_group_id 101 | CLUSTER_VPC_ID = var.cluster_vpc_id 102 | CONTAINER_NAME = var.container_name 103 | TASK_FAMILY = var.task_family 104 | } 105 | } 106 | } 107 | 108 | resource "aws_lambda_permission" "api_gateway" { 109 | statement_id = "AllowExecutionFromAPIGateway" 110 | action = "lambda:InvokeFunction" 111 | function_name = aws_lambda_function.create_bastion.arn 112 | principal = "apigateway.amazonaws.com" 113 | source_arn = "arn:aws:execute-api:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${var.rest_api_id}/*/${var.http_method}${var.resource_path}" 114 | } 115 | -------------------------------------------------------------------------------- /service/api/lambda/trigger-bastion-destruction/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trigger-bastion-destruction", 3 | "version": "0.1.0-SNAPSHOT", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "aws-sdk": { 8 | "version": "2.668.0", 9 | "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.668.0.tgz", 10 | "integrity": "sha512-mmZJmeenNM9hRR4k+JAStBhYFym2+VCPTRWv0Vn2oqqXIaIaNVdNf9xag/WMG8b8M80R3XXfVHKmDPST0/EfHA==", 11 | "dev": true, 12 | "requires": { 13 | "buffer": "4.9.1", 14 | "events": "1.1.1", 15 | "ieee754": "1.1.13", 16 | "jmespath": "0.15.0", 17 | "querystring": "0.2.0", 18 | "sax": "1.2.1", 19 | "url": "0.10.3", 20 | "uuid": "3.3.2", 21 | "xml2js": "0.4.19" 22 | } 23 | }, 24 | "base64-js": { 25 | "version": "1.3.1", 26 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", 27 | "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", 28 | "dev": true 29 | }, 30 | "buffer": { 31 | "version": "4.9.1", 32 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", 33 | "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", 34 | "dev": true, 35 | "requires": { 36 | "base64-js": "^1.0.2", 37 | "ieee754": "^1.1.4", 38 | "isarray": "^1.0.0" 39 | } 40 | }, 41 | "events": { 42 | "version": "1.1.1", 43 | "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", 44 | "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", 45 | "dev": true 46 | }, 47 | "ieee754": { 48 | "version": "1.1.13", 49 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", 50 | "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", 51 | "dev": true 52 | }, 53 | "isarray": { 54 | "version": "1.0.0", 55 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 56 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", 57 | "dev": true 58 | }, 59 | "jmespath": { 60 | "version": "0.15.0", 61 | "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", 62 | "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=", 63 | "dev": true 64 | }, 65 | "punycode": { 66 | "version": "1.3.2", 67 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", 68 | "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", 69 | "dev": true 70 | }, 71 | "querystring": { 72 | "version": "0.2.0", 73 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 74 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", 75 | "dev": true 76 | }, 77 | "sax": { 78 | "version": "1.2.1", 79 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", 80 | "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=", 81 | "dev": true 82 | }, 83 | "url": { 84 | "version": "0.10.3", 85 | "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", 86 | "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", 87 | "dev": true, 88 | "requires": { 89 | "punycode": "1.3.2", 90 | "querystring": "0.2.0" 91 | } 92 | }, 93 | "uuid": { 94 | "version": "3.3.2", 95 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 96 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", 97 | "dev": true 98 | }, 99 | "xml2js": { 100 | "version": "0.4.19", 101 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", 102 | "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", 103 | "dev": true, 104 | "requires": { 105 | "sax": ">=0.6.0", 106 | "xmlbuilder": "~9.0.1" 107 | } 108 | }, 109 | "xmlbuilder": { 110 | "version": "9.0.7", 111 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", 112 | "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", 113 | "dev": true 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /service/api/lambda/bastion/src/bastion/sg.clj: -------------------------------------------------------------------------------- 1 | (ns bastion.sg 2 | (:require [cognitect.aws.client.api :as aws] 3 | [bastion.clients :as clients] 4 | [bastion.env :as env] 5 | [bastion.util :as util])) 6 | 7 | (defn- authorize-ingress-to-bastion 8 | [security-group-id cidr-ip] 9 | (println "Authorizing ingress from" cidr-ip "to security group" security-group-id) 10 | (aws/invoke @clients/ec2 11 | {:op :AuthorizeSecurityGroupIngress 12 | :request {:CidrIp cidr-ip 13 | :FromPort 22 14 | :GroupId security-group-id 15 | :IpProtocol "tcp" 16 | :ToPort 22}})) 17 | 18 | (defn- authorize-ingress-via-https 19 | [security-group-id] 20 | (println "Authorizing ingress via HTTPS from security group" security-group-id) 21 | (aws/invoke @clients/ec2 22 | {:op :AuthorizeSecurityGroupIngress 23 | :request {:GroupId security-group-id 24 | :IpPermissions [{:FromPort 443 25 | :IpProtocol "tcp" 26 | :ToPort 443 27 | :UserIdGroupPairs [{:GroupId security-group-id}]}]}})) 28 | 29 | (defn- authorize-bastion-ingress-to-default 30 | [security-group-id] 31 | (println "Authorizing ingress from" security-group-id "to default security group" env/cluster-vpc-default-security-group-id) 32 | (aws/invoke @clients/ec2 33 | {:op :AuthorizeSecurityGroupIngress 34 | :request {:GroupId env/cluster-vpc-default-security-group-id 35 | :IpPermissions [{:IpProtocol "-1" 36 | :UserIdGroupPairs [{:GroupId security-group-id}]}]}})) 37 | 38 | (defn- revoke-bastion-ingress-to-default 39 | [security-group-id] 40 | (println "Revoking ingress from" security-group-id "to default security group" env/cluster-vpc-default-security-group-id) 41 | (aws/invoke @clients/ec2 42 | {:op :RevokeSecurityGroupIngress 43 | :request {:GroupId env/cluster-vpc-default-security-group-id 44 | :IpPermissions [{:IpProtocol "-1" 45 | :UserIdGroupPairs [{:GroupId security-group-id}]}]}})) 46 | 47 | (defn ip-matches? 48 | [security-group-id cidr-ip] 49 | (println "Checking whether ingress IP matches" cidr-ip "for security group" security-group-id) 50 | (let [response (aws/invoke @clients/ec2 51 | {:op :DescribeSecurityGroups 52 | :request {:GroupsIds [security-group-id] 53 | :Filters 54 | [{:Name "ip-permission.cidr" 55 | :Values [cidr-ip]}]}})] 56 | (not (nil? (first (:SecurityGroups response)))))) 57 | 58 | (defn get-id-for 59 | [user] 60 | (println "Getting existing security group for" user) 61 | (let [response (aws/invoke @clients/ec2 62 | {:op :DescribeSecurityGroups 63 | :request {:Filters 64 | [{:Name "vpc-id" 65 | :Values [env/cluster-vpc-id]} 66 | {:Name "group-name" 67 | :Values [(util/security-group-name user)]}]}})] 68 | (:GroupId (first (:SecurityGroups response))))) 69 | 70 | (defn create-for 71 | [user cidr-ip] 72 | (println "Creating security group for" user "with ingress from" cidr-ip) 73 | (let [security-group (aws/invoke @clients/ec2 74 | {:op :CreateSecurityGroup 75 | :request {:Description (str "Bastion access to " env/service-name " for " user) 76 | :GroupName (util/security-group-name user) 77 | :VpcId env/cluster-vpc-id}}) 78 | security-group-id (:GroupId security-group)] 79 | (authorize-ingress-to-bastion security-group-id cidr-ip) 80 | (authorize-ingress-via-https security-group-id) 81 | (authorize-bastion-ingress-to-default security-group-id) 82 | security-group-id)) 83 | 84 | (defn delete-for 85 | [user] 86 | (if-let [security-group-id (get-id-for user)] 87 | (do 88 | (revoke-bastion-ingress-to-default security-group-id) 89 | (println "Deleting security group for" user) 90 | (aws/invoke @clients/ec2 91 | {:op :DeleteSecurityGroup 92 | :request {:GroupId security-group-id}})))) 93 | -------------------------------------------------------------------------------- /service/api/bastion.tf: -------------------------------------------------------------------------------- 1 | resource "aws_api_gateway_resource" "bastion" { 2 | parent_id = aws_api_gateway_rest_api.bastion.root_resource_id 3 | path_part = "bastion" 4 | rest_api_id = aws_api_gateway_rest_api.bastion.id 5 | } 6 | 7 | resource "aws_api_gateway_model" "bastion" { 8 | rest_api_id = aws_api_gateway_rest_api.bastion.id 9 | name = "Bastion" 10 | content_type = "application/json" 11 | 12 | schema = <<-EOT 13 | { 14 | "$schema": "http://json-schema.org/draft-04/schema#", 15 | "title": "Bastion", 16 | "type": "object", 17 | "additionalProperties": false, 18 | "properties": { 19 | "ip": { 20 | "type": "string" 21 | } 22 | }, 23 | "required": [ 24 | "ip" 25 | ] 26 | } 27 | EOT 28 | } 29 | 30 | # 31 | # POST /bastion 32 | # 33 | 34 | resource "aws_api_gateway_method" "post_bastion" { 35 | authorization = "AWS_IAM" 36 | http_method = "POST" 37 | resource_id = aws_api_gateway_resource.bastion.id 38 | rest_api_id = aws_api_gateway_rest_api.bastion.id 39 | } 40 | 41 | resource "aws_api_gateway_method_response" "post_bastion" { 42 | depends_on = [ 43 | aws_api_gateway_method.post_bastion] 44 | http_method = "POST" 45 | resource_id = aws_api_gateway_resource.bastion.id 46 | 47 | response_models = { 48 | "application/json" = aws_api_gateway_model.bastion.name 49 | } 50 | 51 | rest_api_id = aws_api_gateway_rest_api.bastion.id 52 | status_code = "201" 53 | } 54 | 55 | module "create_bastion_function" { 56 | source = "./lambda/create-bastion" 57 | 58 | cluster_arn = var.cluster_arn 59 | cluster_name = var.cluster_name 60 | cluster_subnet_ids = var.cluster_subnet_ids 61 | cluster_vpc_default_security_group_id = var.cluster_vpc_default_security_group_id 62 | cluster_vpc_id = var.cluster_vpc_id 63 | container_name = var.container_name 64 | execution_role_arn = var.execution_role_arn 65 | http_method = aws_api_gateway_method.post_bastion.http_method 66 | resource_path = aws_api_gateway_resource.bastion.path 67 | rest_api_id = aws_api_gateway_rest_api.bastion.id 68 | task_family = var.task_family 69 | task_role_arn = var.task_role_arn 70 | } 71 | 72 | resource "aws_api_gateway_integration" "post_bastion" { 73 | http_method = aws_api_gateway_method.post_bastion.http_method 74 | resource_id = aws_api_gateway_resource.bastion.id 75 | rest_api_id = aws_api_gateway_rest_api.bastion.id 76 | type = "AWS_PROXY" 77 | integration_http_method = "POST" 78 | uri = "arn:aws:apigateway:${data.aws_region.current.name}:lambda:path/2015-03-31/functions/${module.create_bastion_function.function_arn}/invocations" 79 | } 80 | 81 | # 82 | # DELETE /bastion 83 | # 84 | 85 | resource "aws_api_gateway_method" "delete_bastion" { 86 | authorization = "AWS_IAM" 87 | http_method = "DELETE" 88 | resource_id = aws_api_gateway_resource.bastion.id 89 | rest_api_id = aws_api_gateway_rest_api.bastion.id 90 | } 91 | 92 | resource "aws_api_gateway_method_response" "delete_bastion" { 93 | depends_on = [ 94 | aws_api_gateway_method.delete_bastion] 95 | http_method = "POST" 96 | resource_id = aws_api_gateway_resource.bastion.id 97 | 98 | response_models = { 99 | "application/json" = "Empty" 100 | } 101 | 102 | rest_api_id = aws_api_gateway_rest_api.bastion.id 103 | status_code = "200" 104 | } 105 | 106 | module "destroy_bastion_function" { 107 | source = "./lambda/destroy-bastion" 108 | 109 | cluster_arn = var.cluster_arn 110 | cluster_name = var.cluster_name 111 | cluster_vpc_default_security_group_id = var.cluster_vpc_default_security_group_id 112 | cluster_vpc_id = var.cluster_vpc_id 113 | } 114 | 115 | module "trigger_bastion_destruction_function" { 116 | source = "./lambda/trigger-bastion-destruction" 117 | 118 | destroy_bastion_function_arn = module.destroy_bastion_function.function_arn 119 | destroy_bastion_function_name = module.destroy_bastion_function.function_name 120 | http_method = aws_api_gateway_method.delete_bastion.http_method 121 | resource_path = aws_api_gateway_resource.bastion.path 122 | rest_api_id = aws_api_gateway_rest_api.bastion.id 123 | } 124 | 125 | resource "aws_api_gateway_integration" "delete_bastion" { 126 | http_method = aws_api_gateway_method.delete_bastion.http_method 127 | resource_id = aws_api_gateway_resource.bastion.id 128 | rest_api_id = aws_api_gateway_rest_api.bastion.id 129 | type = "AWS_PROXY" 130 | integration_http_method = "POST" 131 | uri = "arn:aws:apigateway:${data.aws_region.current.name}:lambda:path/2015-03-31/functions/${module.trigger_bastion_destruction_function.function_arn}/invocations" 132 | } 133 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor to control, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of New York and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | --------------------------------------------------------------------------------