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/0009_set_and_encrypt_ro_if_not_exists.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0010_set_datetime_precision.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/0011_create_api_client.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0012_seed_default_platform_api_client.xml" relativeToChangelogFile="true"/>
|
||||||
</databaseChangeLog>
|
</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