Skip to content

Commit

Permalink
Merge pull request #2 from G-Research/astral-poc
Browse files Browse the repository at this point in the history
Astral poc
  • Loading branch information
suprjinx authored Aug 22, 2024
2 parents 187ebec + 2db0de8 commit 9caebcf
Show file tree
Hide file tree
Showing 20 changed files with 469 additions and 92 deletions.
8 changes: 4 additions & 4 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@
"name": "Astral-Rails",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}"
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",

// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// This can be used to network with other containers or the host.
// "forwardPorts": [3000, 5432, 8200]
"forwardPorts": [3000, 5432, 8200],

// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "bundle install && rake db:setup",
"postCreateCommand": "bundle install && rake vault:setup",

// Configure tool-specific properties.
// "customizations": {},

// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
"remoteUser": "vscode"
}
46 changes: 15 additions & 31 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,44 +13,28 @@ services:
command: sleep infinity

# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
network_mode: service:db
networks:
astral:
ipv4_address: "10.1.10.200"

environment:
VAULT_ADDRESS: http://vault:8200
VAULT_ADDR: http://10.1.10.100:8200
VAULT_TOKEN: root_token
JWT_SIGNING_KEY: jwt_secret

vault:
image: hashicorp/vault:latest
restart: unless-stopped
environment:
VAULT_DEV_ROOT_TOKEN_ID: root_token
VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200

db:
image: postgres:latest
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
- ./create-db-user.sql:/docker-entrypoint-initdb.d/create-db-user.sql
environment:
POSTGRES_USER: postgres
POSTGRES_DB: postgres
POSTGRES_PASSWORD: postgres
# Your config/database.yml should use the user and password you set here,
# and host "db" (as that's the name of this service). You can use whatever
# database name you want. Use `bin/rails db:prepare` to create the database.
#
# Example:
#
# development:
# <<: *default
# host: db
# username: postgres
# password: postgres
# database: myapp_development

# Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)

volumes:
postgres-data:
networks:
astral:
ipv4_address: "10.1.10.100"

networks:
astral:
ipam:
driver: default
config:
- subnet: "10.1.10.0/24"
42 changes: 6 additions & 36 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true

- name: Scan for common Rails security vulnerabilities using static analysis
run: bin/brakeman --no-pager

scan_js:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
- name: Run brakeman
uses: devcontainers/[email protected]
with:
ruby-version: .ruby-version
bundler-cache: true

- name: Scan for security vulnerabilities in JavaScript dependencies
run: bin/importmap audit
runCmd: bin/brakeman --no-pager

lint:
runs-on: ubuntu-latest
Expand All @@ -63,23 +43,13 @@ jobs:
# - 6379:6379
# options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- name: Install packages
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y google-chrome-stable curl libjemalloc2 libsqlite3-0 libvips

- name: Checkout code
uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true

- name: Run tests
env:
RAILS_ENV: test
# REDIS_URL: redis://localhost:6379/0
run: bin/rails db:test:prepare test test:system
uses: devcontainers/[email protected]
with:
runCmd: bin/rails test

- name: Keep screenshots from failed system tests
uses: actions/upload-artifact@v4
Expand Down
7 changes: 5 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ gem "puma", ">= 5.0"
# gem "bcrypt", "~> 3.1.7"

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ windows jruby ]
gem "tzinfo-data", platforms: %i[ mswin jruby ]

# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false
Expand All @@ -32,9 +32,12 @@ gem "bootsnap", require: false
# Use the vault-ruby gem to interact with HashiCorp Vault
gem "vault"

# Use the jwt gem to decode access tokens
gem "jwt"

group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"
gem "debug", platforms: %i[ mri mswin ], require: "debug/prelude"

# Static analysis for security vulnerabilities [https://brakemanscanner.org/]
gem "brakeman", require: false
Expand Down
5 changes: 4 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ GEM
bigdecimal (3.1.8)
bootsnap (1.18.4)
msgpack (~> 1.2)
brakeman (6.1.2)
brakeman (6.2.1)
racc
builder (3.3.0)
concurrent-ruby (1.3.4)
Expand All @@ -100,6 +100,8 @@ GEM
rdoc (>= 4.0.0)
reline (>= 0.4.2)
json (2.7.2)
jwt (2.8.2)
base64
language_server-protocol (3.17.0.3)
logger (1.6.0)
loofah (2.22.0)
Expand Down Expand Up @@ -268,6 +270,7 @@ DEPENDENCIES
bootsnap
brakeman
debug
jwt
puma (>= 5.0)
rails (~> 7.2.0)
rubocop-rails-omakase
Expand Down
35 changes: 18 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
# README

This README would normally document whatever steps are necessary to get the
application up and running.
Astral-rails is a proof-of-concept api application intended to simplify
certificate acquisition for other applications/services. Broadly speaking,
it will:

Things you may want to cover:
1) Authorize the request for cerficate using a third party trusted source (JWT, etc)
2) If authorized, obtain a certificate from PKI CLM (such as Vault/OpenBao)
3) Log this transaction in audit infrastructure (ELK, etc).

* Ruby version
# Running

* System dependencies
This app is most easily run and developed in its devcontainer.

* Configuration
1) Open in devcontainer
2) Launch server using vscode launch config, or in terminal run:
```
rails s
```
3) POST /certificates to acquire cert in terminal:
```
curl -X POST http://localhost:3000/certificates \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhcHBsaWNhdGlvbl9uYW1lIiwiY29tbW9uX25hbWUiOiJleGFtcGxlLmNvbSIsImlwX3NhbnMiOiIxMC4wLjEuMTAwIn0.61e0oQIj7vwGtOpFuPJDCI_Bqf8ZTpJxe_2kUwcbN7Y"
```

* Database creation

* Database initialization

* How to run the test suite

* Services (job queues, cache servers, search engines, etc.)

* Deployment instructions

* ...
33 changes: 33 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,35 @@
class ApplicationController < ActionController::API
rescue_from StandardError, with: :handle_standard_error
rescue_from AuthError, with: :handle_auth_error
rescue_from ActionController::ParameterMissing, with: :handle_bad_request

attr_reader :identity # decoded and verified JWT

def info
render json: {
app: "astral",
description: "Astral provides a simplified API for PKI.",
version: "0.0.1"
}
end

def authenticate_request
token = request.headers["Authorization"]
token = token.split(" ").last if token
@identity = Services::AuthService.new.authenticate!(token)
end

private

def handle_standard_error(exception)
render json: { error: exception.message }, status: :internal_server_error
end

def handle_auth_error(exception)
render json: { error: "Unauthorized" }, status: :unauthorized
end

def handle_bad_request(exception)
render json: { error: exception }, status: :bad_request
end
end
36 changes: 36 additions & 0 deletions app/controllers/certificates_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
class CertificatesController < ApplicationController
before_action :authenticate_request

def create
req = CertIssueRequest.new(params_permitted)
if !req.valid?
render json: { error: req.errors }, status: :bad_request
else
cert = Services::CertificateService.new.issue_cert(req)
render json: cert
end
end

private

def params_permitted
attrs = %i[ common_name
alt_names
exclude_cn_from_sans
format
not_after
other_sans
private_key_format
remove_roots_from_chain
ttl
uri_sans
ip_sans
serial_number
client_flag
code_signing_flag
email_protection_flag
server_flag
]
params.permit(attrs)
end
end
3 changes: 3 additions & 0 deletions app/lib/auth_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Error representing a failed authentication
class AuthError < StandardError
end
32 changes: 32 additions & 0 deletions app/lib/services/app_registry_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
module Services
class AppRegistryService
def authenticate!(token)
identity = decode(token)
raise AuthError unless identity
# TODO verify identity with authority?
identity
end

def authorize!(identity, cert_req)
cert_req.fqdns.each do |fqdn|
domain = get_domain_name(fqdn)
raise AuthError unless (domain[:auto_approved_groups] & identity[:groups]).any?
end
end

private

def decode(token)
# Decode a JWT access token using the configured base.
body = JWT.decode(token, Rails.application.config.astral[:jwt_signing_key])[0]
HashWithIndifferentAccess.new body
rescue => e
Rails.logger.warn "Unable to decode token: #{e}"
nil
end

def get_domain_name(fqdn)
# TODO implement
end
end
end
16 changes: 16 additions & 0 deletions app/lib/services/auth_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module Services
class AuthService
def initialize
# TODO make this selectable
@impl = AppRegistryService.new
end

def authenticate!(token)
@impl.authenticate!(token)
end

def authorize!(token, cert_issue_req)
@impl.authorize!(token, cert_issue_req)
end
end
end
12 changes: 12 additions & 0 deletions app/lib/services/certificate_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module Services
class CertificateService
def initialize
# TODO this should select an implementation service based on config
@impl = VaultService.new
end

def issue_cert(cert_issue_request)
@impl.issue_cert(cert_issue_request)
end
end
end
Loading

0 comments on commit 9caebcf

Please sign in to comment.