feat(security): seed a fixed default platform API key via migration instead of a random one at runtime
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Replaces the previous DefaultApiClientBootstrapper (ApplicationRunner minting a random key on first startup) with a plain Liquibase seed on the tenant-store database: one fixed, well-known api_key_hash row, inserted once via migration 0012, same on every deployment and every tenant -- exactly like this fork already seeds default codes/permissions, rather than generating a secret at runtime. The trade-off is explicit in the migration's own comment: a fixed key checked into source is a bootstrap convenience, not a secret, and any deployment beyond local/dev must rotate it (insert a new hash, deactivate this row) before going live -- rotation is a direct m_api_client write, same as the seed itself, since POST /v1/apiclients stays disabled. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
-83
@@ -1,83 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.security.apiclient;
|
||||
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Mints the very first platform-operator API key on a brand-new deployment, once, at startup -- the
|
||||
* counterpart to {@link ApiClientAdminApiResource} deliberately disabling {@code POST /v1/apiclients}.
|
||||
* With that HTTP path closed for security (see that class's javadoc), a fresh {@code m_api_client}
|
||||
* table with zero rows would otherwise leave a first-time deployer with no way at all to reach
|
||||
* {@code #/platform-admin/menu-access} except hand-writing SQL themselves -- this closes that gap
|
||||
* without reopening the escalation path the disabled endpoint existed to close.
|
||||
*
|
||||
* Runs as an {@link ApplicationRunner}, which Spring Boot only invokes after the context has fully
|
||||
* refreshed -- i.e. strictly after {@code TenantDatabaseUpgradeService} (an {@code InitializingBean})
|
||||
* has already created {@code m_api_client} via migration 0011. The guard is simply "does the table have
|
||||
* any row yet": on every startup after the first, this is a no-op, so it never mints a second key
|
||||
* behind anyone's back.
|
||||
*
|
||||
* The raw key is deliberately never written to the database (only its hash is, same as any other key)
|
||||
* and never returned over HTTP -- it is logged ONCE, here, to this server's own application log. That
|
||||
* is a real security boundary, not a formality: reading it requires access to the server's own log
|
||||
* output, which is a materially different (and much smaller) trust boundary than "reachable over the
|
||||
* network," which is exactly why the HTTP minting path was closed instead of left open.
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DefaultApiClientBootstrapper implements ApplicationRunner {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DefaultApiClientBootstrapper.class);
|
||||
|
||||
private final ApiClientCredentialService credentialService;
|
||||
|
||||
@Override
|
||||
public void run(final ApplicationArguments args) {
|
||||
if (!credentialService.listClients().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
final Map<String, Object> created = credentialService.createClient("Default Platform Operator (auto-created on first startup)",
|
||||
null, null);
|
||||
final String rawKey = (String) created.get("apiKey");
|
||||
|
||||
LOG.warn("""
|
||||
|
||||
================================================================================
|
||||
CBSNexora: first-run platform operator API key created.
|
||||
|
||||
This key unlocks #/platform-admin/menu-access on this deployment. It is stored
|
||||
only as a hash in m_api_client -- this is the ONLY place the raw value is ever
|
||||
shown. Copy it now; it cannot be recovered later, only reissued as a new key.
|
||||
|
||||
API Key: {}
|
||||
|
||||
Minting a replacement over HTTP is deliberately disabled (see
|
||||
ApiClientAdminApiResource) -- a further key can only be inserted directly into
|
||||
m_api_client in the tenant-store database, the same way this one was.
|
||||
================================================================================
|
||||
""", rawKey);
|
||||
}
|
||||
}
|
||||
+1
@@ -32,4 +32,5 @@
|
||||
<include file="parts/0009_set_and_encrypt_ro_if_not_exists.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0010_set_datetime_precision.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0011_create_api_client.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0012_seed_default_platform_api_client.xml" relativeToChangelogFile="true"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
-->
|
||||
<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.1.xsd">
|
||||
|
||||
<!-- Seeds ONE fixed, well-known platform-operator API key on every deployment, so
|
||||
#/platform-admin/menu-access works out of the box the first time the app is stood up,
|
||||
without anyone needing shell/DB access to mint the first key by hand. It is deliberately
|
||||
the SAME raw value on every deployment/tenant (requested behaviour) rather than a random
|
||||
one generated per install -- the trade-off, on purpose: this value is visible to anyone who
|
||||
can read this source file, exactly like a default admin password shipped in a seed script.
|
||||
It is a bootstrap convenience, not a secret. Rotate it for any deployment that is not purely
|
||||
local/dev: insert a new row with a real random key's hash (see ApiClientCredentialService's
|
||||
hashing) and either deactivate or DELETE this row -- POST /v1/apiclients is intentionally
|
||||
disabled (see ApiClientAdminApiResource), so rotation is a direct m_api_client write, same
|
||||
as this seed.
|
||||
|
||||
Raw key (only shown here, since it is fixed and not a secret to rotate away from):
|
||||
cbsx_default_platform_operator_key_2026
|
||||
api_key_hash below is SHA-256 of that exact string, hex-encoded, lowercase -- matching
|
||||
ApiClientCredentialService#sha256Hex. -->
|
||||
<changeSet id="0012_seed_default_platform_api_client" author="cbs-nitesh">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<and>
|
||||
<tableExists tableName="m_api_client"/>
|
||||
<sqlCheck expectedResult="0">
|
||||
SELECT COUNT(*) FROM m_api_client
|
||||
WHERE api_key_hash = '9d6d4dfa7a1953e33e0acde4fd775aad11689ec540340c7224796447118a3edd'
|
||||
</sqlCheck>
|
||||
</and>
|
||||
</preConditions>
|
||||
<insert tableName="m_api_client">
|
||||
<column name="client_name" value="Default Platform Operator (rotate before real production use)"/>
|
||||
<column name="api_key_hash" value="9d6d4dfa7a1953e33e0acde4fd775aad11689ec540340c7224796447118a3edd"/>
|
||||
<column name="api_key_prefix" value="cbsx_default"/>
|
||||
<column name="allowed_ips"/>
|
||||
<column name="created_by"/>
|
||||
<column name="created_by_tenant" value="ALL"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
Reference in New Issue
Block a user