ops: add CI/CD pipeline, a/b rolling deploy, Gitea Actions workflow
Deploy to Production / deploy (push) Failing after 10s
Deploy to Production / deploy (push) Failing after 10s
- .gitea/workflows/deploy.yml — push-to-main triggers rolling deploy - scripts/deploy-bluegreen.sh — a-stack then b-stack restart; Maven runs in Docker (no JDK needed on runner host); Caddy reload at end - scripts/deploy-all.ps1 — emergency manual deploy from dev machine - infra/docker-compose.yml — a/b pairs per service; wget health checks; Gitea service; Prometheus/Grafana/DB ports restricted to localhost - infra/Caddyfile — dual upstreams with health-based routing - infra/Dockerfile.* — one per service - infra/prometheus.yml + grafana provisioning Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,8 +23,22 @@ quarkus.http.port=8180
|
||||
apix.api-key=${APIX_API_KEY:dev-insecure-key-change-in-prod}
|
||||
|
||||
# ── Verification ──────────────────────────────────────────────────────────────
|
||||
apix.dns.doh-url=${APIX_DOH_URL:https://dns.google/resolve}
|
||||
apix.gleif.api-url=${GLEIF_API_URL:https://api.gleif.org/api/v1}
|
||||
apix.opencorporates.api-key=${OPENCORPORATES_API_KEY:}
|
||||
apix.opencorporates.api-url=${OPENCORPORATES_API_URL:https://api.opencorporates.com/v0.4}
|
||||
apix.hygiene.security-txt-url-template=${APIX_SECURITY_TXT_TEMPLATE:https://{domain}/.well-known/security.txt}
|
||||
apix.verification.http-timeout-ms=${APIX_VERIFICATION_TIMEOUT_MS:5000}
|
||||
apix.org.tan.expose-in-response=false
|
||||
|
||||
# ── Mail signing (Ed25519) ────────────────────────────────────────────────────
|
||||
# PKCS#8 private key and SubjectPublicKeyInfo public key, Base64 standard encoding.
|
||||
# If blank, an ephemeral key pair is generated at startup (dev/test only).
|
||||
apix.mail.signing.private-key-base64=${APIX_MAIL_SIGNING_PRIVATE_KEY:}
|
||||
apix.mail.signing.public-key-base64=${APIX_MAIL_SIGNING_PUBLIC_KEY:}
|
||||
# Key ID published in signed payloads and the /mail-signing-keys endpoint.
|
||||
# Convention: YYYY-MM, rotated every 6 months.
|
||||
apix.mail.signing.kid=${APIX_MAIL_SIGNING_KID:dev}
|
||||
apix.sanctions.cache-path=${SANCTIONS_CACHE_PATH:./sanctions-cache}
|
||||
|
||||
# ── Logging ───────────────────────────────────────────────────────────────────
|
||||
@@ -33,3 +47,30 @@ quarkus.log.console.json=false
|
||||
|
||||
# ── Health ────────────────────────────────────────────────────────────────────
|
||||
quarkus.smallrye-health.root-path=/q/health
|
||||
|
||||
# ── Observability ─────────────────────────────────────────────────────────────
|
||||
# HTTP server request metrics (http_server_requests_seconds histogram) are
|
||||
# auto-instrumented by quarkus-micrometer. Prometheus scrapes /q/metrics.
|
||||
quarkus.micrometer.enabled=true
|
||||
quarkus.micrometer.export.prometheus.enabled=true
|
||||
# Without match-patterns, the uri label is the raw request path and UUIDs create
|
||||
# unbounded cardinality. Order matters: specific sub-paths before the catch-all.
|
||||
quarkus.micrometer.binder.http-server.match-patterns=\
|
||||
/services/[0-9a-f-]+/replacements=/services/{id}/replacements,\
|
||||
/services/[0-9a-f-]+/history=/services/{id}/history,\
|
||||
/services/[0-9a-f-]+/olevel=/services/{id}/olevel,\
|
||||
/services/[0-9a-f-]+=/services/{id},\
|
||||
/organizations/[0-9a-f-]+/verify=/organizations/{id}/verify,\
|
||||
/organizations/[0-9a-f-]+/request-upgrade=/organizations/{id}/request-upgrade,\
|
||||
/organizations/[0-9a-f-]+/earned-level=/organizations/{id}/earned-level,\
|
||||
/organizations/[0-9a-f-]+/temp-grant=/organizations/{id}/temp-grant,\
|
||||
/organizations/[0-9a-f-]+/rotate-key=/organizations/{id}/rotate-key,\
|
||||
/organizations/[0-9a-f-]+/request-tan=/organizations/{id}/request-tan,\
|
||||
/organizations/[0-9a-f-]+/rotate-key-with-tan=/organizations/{id}/rotate-key-with-tan,\
|
||||
/organizations/[0-9a-f-]+/rotate-key-dns=/organizations/{id}/rotate-key-dns,\
|
||||
/organizations/[0-9a-f-]+/confirm-key-rotation-dns=/organizations/{id}/confirm-key-rotation-dns,\
|
||||
/organizations/[0-9a-f-]+/notify-key-rotation-fraud=/organizations/{id}/notify-key-rotation-fraud,\
|
||||
/organizations/[0-9a-f-]+/fraud-lock=/organizations/{id}/fraud-lock,\
|
||||
/organizations/[0-9a-f-]+/audit-log=/organizations/{id}/audit-log,\
|
||||
/organizations/[0-9a-f-]+/event-feed=/organizations/{id}/event-feed,\
|
||||
/organizations/[0-9a-f-]+=/organizations/{id}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
|
||||
|
||||
<changeSet id="010" author="apix">
|
||||
|
||||
<createTable tableName="organizations">
|
||||
<column name="id" type="uuid" defaultValueComputed="gen_random_uuid()">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
<column name="registrant_name" type="varchar(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="registrant_email" type="varchar(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="registrant_jurisdiction" type="varchar(10)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="registrant_org_type" type="varchar(50)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<!-- Domain used for DNS and hygiene verification -->
|
||||
<column name="domain" type="varchar(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<!-- GLEIF LEI — populated during O-2 if a GLEIF record is found -->
|
||||
<column name="lei" type="varchar(20)"/>
|
||||
|
||||
<!-- Credentials — stored as SHA-256 hex hashes, never in plaintext -->
|
||||
<column name="api_key_hash" type="varchar(64)">
|
||||
<constraints nullable="false" unique="true" uniqueConstraintName="uq_org_api_key_hash"/>
|
||||
</column>
|
||||
<!-- Rotation secret exposure → always triggers admin+TAN process -->
|
||||
<column name="rotation_secret_hash" type="varchar(64)">
|
||||
<constraints nullable="false" unique="true" uniqueConstraintName="uq_org_rotation_secret_hash"/>
|
||||
</column>
|
||||
|
||||
<!-- O-level state machine -->
|
||||
<column name="target_o_level" type="varchar(50)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="earned_o_level" type="varchar(50)" defaultValue="UNVERIFIED">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<!-- PENDING | VERIFYING | ACHIEVED | FAILED | MANUAL_REVIEW -->
|
||||
<column name="verification_status" type="varchar(50)" defaultValue="PENDING">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<!-- Which step is in progress or last failed -->
|
||||
<column name="verification_step" type="varchar(100)"/>
|
||||
<column name="verification_error" type="text"/>
|
||||
<!-- Random token org owner must publish as DNS TXT _apix-verification.{domain} -->
|
||||
<column name="dns_verification_token" type="varchar(64)"/>
|
||||
|
||||
<!-- BSF temporary elevated grant -->
|
||||
<column name="temp_max_o_level" type="varchar(50)"/>
|
||||
<column name="temp_max_expires_at" type="timestamptz"/>
|
||||
<column name="temp_max_granted_by" type="varchar(255)"/>
|
||||
<column name="temp_max_granted_reason" type="text"/>
|
||||
|
||||
<!-- Emergency key rotation via admin TAN (one active TAN per org) -->
|
||||
<column name="pending_tan_hash" type="varchar(64)"/>
|
||||
<column name="pending_tan_expires_at" type="timestamptz"/>
|
||||
<!-- Rate-limit: max 3 TAN requests per 24h -->
|
||||
<column name="tan_request_count_24h" type="integer" defaultValueNumeric="0">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="tan_last_requested_at" type="timestamptz"/>
|
||||
|
||||
<!-- Fraud lock: set by /notify-key-rotation-fraud; blocks all rotation until cleared by BSF admin -->
|
||||
<column name="fraud_locked" type="boolean" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="fraud_locked_at" type="timestamptz"/>
|
||||
|
||||
<column name="created_at" type="timestamptz" defaultValueComputed="now()">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="updated_at" type="timestamptz" defaultValueComputed="now()">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
|
||||
<!-- Append-only audit trail of every O-level change event -->
|
||||
<createTable tableName="org_verification_events">
|
||||
<column name="id" type="uuid" defaultValueComputed="gen_random_uuid()">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
<column name="organization_id" type="uuid">
|
||||
<constraints nullable="false"
|
||||
foreignKeyName="fk_org_event_org"
|
||||
references="organizations(id)"
|
||||
deleteCascade="true"/>
|
||||
</column>
|
||||
<!-- REGISTERED | LEVEL_EARNED | UPGRADE_REQUESTED | VERIFICATION_FAILED |
|
||||
TEMP_GRANTED | TEMP_REVOKED | TEMP_EXPIRED | LEVEL_REVOKED |
|
||||
KEY_ROTATED | TAN_ISSUED | DNS_ROTATION_INITIATED |
|
||||
FRAUD_REPORTED | FRAUD_LOCK_CLEARED -->
|
||||
<column name="event_type" type="varchar(50)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="from_level" type="varchar(50)"/>
|
||||
<column name="to_level" type="varchar(50)"/>
|
||||
<!-- SYSTEM | OWNER | BSF_ADMIN -->
|
||||
<column name="triggered_by" type="varchar(50)"/>
|
||||
<column name="notes" type="text"/>
|
||||
<column name="created_at" type="timestamptz" defaultValueComputed="now()">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
|
||||
<createIndex tableName="org_verification_events" indexName="idx_org_events_org_id">
|
||||
<column name="organization_id"/>
|
||||
</createIndex>
|
||||
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
|
||||
|
||||
<changeSet id="011" author="apix">
|
||||
|
||||
<!-- DNS-challenge key rotation fields (bot-friendly, ACME DNS-01 analogy).
|
||||
Token is stored plaintext — it is a public DNS value, not a secret. -->
|
||||
<addColumn tableName="organizations">
|
||||
<column name="pending_rotation_challenge_token" type="varchar(64)"/>
|
||||
<column name="pending_rotation_challenge_expires_at" type="timestamptz"/>
|
||||
</addColumn>
|
||||
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -14,5 +14,7 @@
|
||||
<include file="changes/007-service-replacements.xml" relativeToChangelogFile="true"/>
|
||||
<include file="changes/008-sunset-at.xml" relativeToChangelogFile="true"/>
|
||||
<include file="changes/009-iot-profiles.xml" relativeToChangelogFile="true"/>
|
||||
<include file="changes/010-organizations.xml" relativeToChangelogFile="true"/>
|
||||
<include file="changes/011-org-rotation-challenge.xml" relativeToChangelogFile="true"/>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
||||
Reference in New Issue
Block a user