Skip to content

dsdugal/certify

Repository files navigation

Certify

A web application to demonstrate an inventory of SSL certificates, with a dashboard that requires authentication and a RESTful API.

Stack

  • Ruby — version in .ruby-version and Gemfile.
  • RailsRails ~8.1 with Turbo, Stimulus, importmap-rails (no Node bundler), and Propshaft.
  • API — versioned JSON under namespace :api in config/routes.rb.
  • Data — SQLite + Active Record; production uses multiple files under storage/config/database.yml.
  • Serving & deploy — Puma; Docker uses Thruster (Dockerfile); deploy with Kamal.
  • Authentication — bcrypt on User; opaque API tokens on Session.
  • Tests & CI — Minitest (bin/rails test); CI: RuboCop, Brakeman, Bundler Audit, importmap audit — config/ci.rb.

Architecture

Browser clients use cookie-backed sessions (Authentication, SessionsController, DashboardController). API clients use Bearer tokens on Session rows via Api::V1::BaseController. Certificates always belong to a User; HTML sign-in may call SampleCertificates once per empty account. Production persists data in SQLite files under storage/ (primary, cache, queue, and cable DBs per config/database.yml).

flowchart TB
  subgraph clients [Clients]
    browser[Browser]
    apiClients[JSON API clients]
  end
  subgraph htmlStack [HTML stack]
    sessionsCtrl[SessionsController]
    dashCtrl[DashboardController]
    authCookie[Authentication and cookie session]
  end
  subgraph apiStack [API v1]
    apiSessionsCtrl[Api::V1::SessionsController]
    apiCertsCtrl[Api::V1::CertificatesController]
  end
  subgraph domain [Domain]
    userModel[User]
    sessionModel[Session]
    certModel[Certificate]
    sampleSvc[SampleCertificates]
  end
  subgraph persistence [Persistence]
    sqlite[SQLite in storage]
  end

  browser --> sessionsCtrl
  browser --> dashCtrl
  browser --> authCookie
  authCookie --> sessionModel
  sessionsCtrl --> userModel
  sessionsCtrl --> sessionModel
  sessionsCtrl --> sampleSvc
  sampleSvc --> certModel
  dashCtrl --> certModel
  apiClients --> apiSessionsCtrl
  apiClients --> apiCertsCtrl
  apiSessionsCtrl --> sessionModel
  apiCertsCtrl --> certModel
  userModel --> sessionModel
  userModel --> certModel
  sessionModel --> sqlite
  certModel --> sqlite
Loading

Terminology

The status chart groups certificates into four buckets:

Label Meaning
Revoked revoked_at is set. These are counted only here, even if the certificate is still within its not-yet-expired dates.
Expired Not revoked and not_after is in the past.
Lapsing Not revoked, still within its validity window by dates, but not_after is within the next 30 days (inclusive of the boundary). Same logic as the Certificate.expiring_soon scope (default 30-day window).
Active Not revoked and not_after is more than 30 days after the current time.

So Lapsing is shorthand on the chart for “expiring soon”—certificates that are not yet calendar-expired but should be renewed or replaced soon.

Dashboard

The body of the page is a chart of how many certificates you have in each status bucket—Revoked, Expired, Lapsing, and Active. Each slice’s size is the number of certs in that bucket.

API

The v1 surface follows resource-oriented URLs and maps HTTP verbs to CRUD-style operations, with JSON request and response bodies and conventional HTTP status codes. Routes live under the versioned prefix /api/v1 with format: :json as the default (config/routes.rb).

Api::V1 controllers inherit from ActionController::API with JSON as the default format. Jbuilder is in the Gemfile if you add .json.jbuilder views; certificate endpoints today build response bodies as hashes in Api::V1::CertificatesController.

Each request is self-describing: authentication is sent as an Authorization: Bearer header (no server-side coupling to the previous response except the token value you store). That matches common RESTful JSON API practice even though it is not “hypermedia” (HATEOAS) and there is no machine-generated OpenAPI document in this repository—reasonable tradeoffs for a small API.

Singleton session resourceresource :session, only: %i[create destroy] treats “the current session” as one logical resource per client:

HTTP verb Path Meaning
POST /api/v1/session Create a new Session (authenticate with email/password); 201 Created with a token on success.
DELETE /api/v1/session End the session identified by Authorization: Bearer …; 204 No Content on success.

Certificate collectionresources :certificates exposes certificates as a collection scoped to the authenticated user:

HTTP verb Path Meaning
GET /api/v1/certificates List certificates (index). Optional query ?status=active, expired, or revoked filters the collection.
POST /api/v1/certificates Create a certificate (201 Created with body on success).
GET /api/v1/certificates/:id Read one certificate (200 OK).
PATCH / PUT /api/v1/certificates/:id Update fields (200 OK with body, or 422 on validation errors).
DELETE /api/v1/certificates/:id Delete a certificate (204 No Content).

Use

Running the application

  • Ruby: see .ruby-version and the ruby line in the Gemfile.
  • Setup: bin/setup (or bin/rails db:prepare and bin/rails db:seed as needed).
  • Run: bin/rails server (or bin/dev), then open the app URL (e.g. http://localhost:3000).
  • Tests: bin/rails test

Using the API

  • Obtain a token with POST /api/v1/session (JSON body with email_address and password). Revoke it with DELETE /api/v1/session and the same Authorization header.
  • Tokens expire after 30 days (Session#expires_at); expired tokens are rejected and removed.
  • Certificate bodies nest attributes under a certificate root key. See Api::V1::CertificatesController for permitted fields.

Demo data (HTML sign-in)

Certificates are scoped per user. After a successful browser sign-in, if that user has no certificate rows yet, the app inserts 10 sample certificates for that user only. Signing out deletes that user’s certificates (other users are unaffected). The JSON API does not insert or clear demo data on POST /api/v1/session.

Seeds

  • Development: creates admin@example.com with password from ADMIN_PASSWORD, defaulting to password when the variable is unset.
  • Production: seeds create no default admin. Set ADMIN_EMAIL and ADMIN_PASSWORD to bootstrap one account.

Security hardening

Authorization and data isolation

  • Tenant boundary: Every Certificate belongs to a User (user_id is required).
  • API: Api::V1::CertificatesController only loads and mutates current_user.certificates. Another user’s id returns 404, not another tenant’s row.
  • Dashboard: The inventory chart uses current_user.certificates only (DashboardController).
  • Sign-out: Browser sign-out deletes only the signed-in user’s certificates, then destroys the Session row (SessionsController, Authentication).
  • Demo data: Sample rows are created only on HTML sign-in when that user has no certificates yet; the JSON API does not insert or wipe certificate data on POST /api/v1/session.

Session and token lifecycle

  • Browser cookie: session_id is signed, HttpOnly, SameSite=Lax, and Secure in production (Authentication#start_new_session_for). Expired sessions are dropped when the cookie is presented (Authentication#find_session_by_cookie).
  • Absolute expiry: API and HTML sessions share the same sessions table with expires_at (default 30 days from creation). Expired Bearer tokens are rejected and the session row is removed (Api::V1::BaseController).
  • Revocation: API clients can end a token with DELETE /api/v1/session using the same Authorization: Bearer header.

Abuse controls

Transport, host, and browser policy

  • TLS: Production sets assume_ssl, force_ssl, and HSTS via Rails; GET /up is excluded from the HTTP→HTTPS redirect.
  • Host header: When RAILS_ALLOWED_HOSTS is set, config/environments/production.rb enables explicit config.hosts and excludes /up from host authorization checks.
  • CSP: A Content-Security-Policy is applied with nonces for script-src. There is no broad CORS configuration; the HTML app is same-origin and the API is expected to be used server-to-server or from trusted clients you configure yourself.

Operational hygiene

  • Logs: Request parameter filtering includes passwords, tokens, and related keys (filter_parameter_logging.rb).
  • Bootstrap users: Production does not create a default admin in seeds unless ADMIN_EMAIL and ADMIN_PASSWORD are set.

CI and dependency checks

config/ci.rb runs RuboCop, Bundler Audit, Importmap audit, Brakeman (warnings fail the step), and the full test suite including db:seed:replant in the test environment. Brakeman currently runs with --except EOLRuby until the Ruby runtime is upgraded past the scanner’s end-of-life warning for the version in .ruby-version.

Residual risks (compromised host, stolen laptops, zero-days) still require process and infrastructure controls beyond this application.

About

A web application to track SSL certificates and their status.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors