infra(seed): dump encrypted secret columns as NULL, restore via CLI
ASP.NET Data Protection ciphertext is non-deterministic and bound to the source key ring, so encrypted secret columns (ExternalSystemDefinitions .AuthConfiguration, SmtpConfigurations.Credentials, DatabaseConnection Definitions.ConnectionString) cannot be replayed from a static SQL dump — the app would fail to decrypt them. dump_seed.py now emits those columns as NULL; reseed.sh adds a post-seed stage that recreates the values through the ScadaLink CLI so the EF value converter re-encrypts against the target cluster's key ring.
This commit is contained in:
@@ -183,8 +183,9 @@ INSERT INTO [DataConnections] ([Id], [Name], [Protocol], [PrimaryConfiguration],
|
|||||||
SET IDENTITY_INSERT [DataConnections] OFF;
|
SET IDENTITY_INSERT [DataConnections] OFF;
|
||||||
|
|
||||||
-- ExternalSystemDefinitions (1 rows)
|
-- ExternalSystemDefinitions (1 rows)
|
||||||
|
-- NOTE: [AuthConfiguration] is an encrypted secret column — dumped as NULL. Restore via the app (CLI/API) post-seed.
|
||||||
SET IDENTITY_INSERT [ExternalSystemDefinitions] ON;
|
SET IDENTITY_INSERT [ExternalSystemDefinitions] ON;
|
||||||
INSERT INTO [ExternalSystemDefinitions] ([Id], [Name], [EndpointUrl], [AuthType], [AuthConfiguration], [MaxRetries], [RetryDelay]) VALUES (1, N'Test REST API', N'http://scadalink-restapi:5200', N'ApiKey', N'scadalink-test-key-1', 0, '00:00:00.000000');
|
INSERT INTO [ExternalSystemDefinitions] ([Id], [Name], [EndpointUrl], [AuthType], [AuthConfiguration], [MaxRetries], [RetryDelay]) VALUES (1, N'Test REST API', N'http://scadalink-restapi:5200', N'ApiKey', NULL, 0, '00:00:00.000000');
|
||||||
SET IDENTITY_INSERT [ExternalSystemDefinitions] OFF;
|
SET IDENTITY_INSERT [ExternalSystemDefinitions] OFF;
|
||||||
|
|
||||||
-- ExternalSystemMethods (1 rows)
|
-- ExternalSystemMethods (1 rows)
|
||||||
|
|||||||
@@ -114,6 +114,34 @@ docker exec -i scadalink-mssql /opt/mssql-tools18/bin/sqlcmd \
|
|||||||
-S localhost -U sa -P 'ScadaLink_Dev1#' -C -d ScadaLinkConfig -b < "$SEED_FILE"
|
-S localhost -U sa -P 'ScadaLink_Dev1#' -C -d ScadaLinkConfig -b < "$SEED_FILE"
|
||||||
echo " Seed replayed."
|
echo " Seed replayed."
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- Stage 6d/6: restore encrypted secret config (CLI) ---"
|
||||||
|
# Configuration that lives in encrypted secret columns cannot be replayed from
|
||||||
|
# raw SQL: ASP.NET Data Protection ciphertext is non-deterministic and bound to
|
||||||
|
# the source key ring. Create/restore it through the app so the EF value
|
||||||
|
# converter encrypts against this cluster's key ring.
|
||||||
|
CLI="dotnet run --project $PROJECT_ROOT/src/ScadaLink.CLI --"
|
||||||
|
AUTH="--username multi-role --password password"
|
||||||
|
|
||||||
|
# ExternalSystemDefinitions Id 1 ("Test REST API") is inserted by the seed with
|
||||||
|
# a fixed identity but a NULL AuthConfiguration; set the API key here.
|
||||||
|
$CLI --url "$MGMT_URL" $AUTH external-system update \
|
||||||
|
--id 1 \
|
||||||
|
--name "Test REST API" \
|
||||||
|
--endpoint-url "http://scadalink-restapi:5200" \
|
||||||
|
--auth-type ApiKey \
|
||||||
|
--auth-config "scadalink-test-key-1"
|
||||||
|
echo " External-system auth config restored (encrypted)."
|
||||||
|
|
||||||
|
# The "Machine Data DB" database connection is referenced by name from the
|
||||||
|
# seeded TestDatabaseQuery script. It is not in seed-config.sql (its
|
||||||
|
# ConnectionString is an encrypted secret column); create it through the app.
|
||||||
|
$CLI --url "$MGMT_URL" $AUTH db-connection create \
|
||||||
|
--name "Machine Data DB" \
|
||||||
|
--connection-string "Server=scadalink-mssql,1433;Database=ScadaLinkMachineData;User Id=scadalink_app;Password=ScadaLink_Dev1#;TrustServerCertificate=true" \
|
||||||
|
|| echo " (Machine Data DB connection may already exist)"
|
||||||
|
echo " Database connection created (encrypted)."
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Reseed complete ==="
|
echo "=== Reseed complete ==="
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ Excluded by design (per-environment, not design-time): Sites (seeded via
|
|||||||
seed-sites.sh), Instances + InstanceConnectionBindings + InstanceOverrides,
|
seed-sites.sh), Instances + InstanceConnectionBindings + InstanceOverrides,
|
||||||
NotificationLists/Recipients, SmtpConfigurations, ApiKeys, Areas,
|
NotificationLists/Recipients, SmtpConfigurations, ApiKeys, Areas,
|
||||||
SiteScopeRules, LdapGroupMappings, DataProtectionKeys, audit, deployment.
|
SiteScopeRules, LdapGroupMappings, DataProtectionKeys, audit, deployment.
|
||||||
|
|
||||||
|
Encrypted secret columns (see ENCRYPTED_COLUMNS) are emitted as NULL: they
|
||||||
|
hold ASP.NET Data Protection ciphertext, which is non-deterministic and bound
|
||||||
|
to the source key ring, so a raw SQL dump can never replay a valid value.
|
||||||
|
Re-populate them through the application after the seed runs (infra/reseed.sh
|
||||||
|
does this via the ScadaLink CLI).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -45,6 +51,18 @@ INSERT_ORDER = [
|
|||||||
# the column list. All listed tables happen to use Id as their identity.
|
# the column list. All listed tables happen to use Id as their identity.
|
||||||
IDENTITY_TABLES = set(INSERT_ORDER)
|
IDENTITY_TABLES = set(INSERT_ORDER)
|
||||||
|
|
||||||
|
# (table, column) pairs encrypted at rest via ASP.NET Data Protection
|
||||||
|
# (EncryptedStringConverter in ScadaLink.ConfigurationDatabase). Ciphertext is
|
||||||
|
# non-deterministic and key-ring-bound, so it cannot be replayed from a static
|
||||||
|
# SQL dump — the application would fail to decrypt it on read. These columns
|
||||||
|
# are dumped as NULL; re-seed their values through the app (CLI / API) so the
|
||||||
|
# value converter encrypts them against the target key ring.
|
||||||
|
ENCRYPTED_COLUMNS = {
|
||||||
|
("ExternalSystemDefinitions", "AuthConfiguration"),
|
||||||
|
("SmtpConfigurations", "Credentials"),
|
||||||
|
("DatabaseConnectionDefinitions", "ConnectionString"),
|
||||||
|
}
|
||||||
|
|
||||||
# Templates has self-FK Templates.ParentTemplateId; emit a single batch that
|
# Templates has self-FK Templates.ParentTemplateId; emit a single batch that
|
||||||
# inserts shallow rows first then deeper ones. pymssql returns rows in Id order
|
# inserts shallow rows first then deeper ones. pymssql returns rows in Id order
|
||||||
# from our ORDER BY, which matches insertion order for this schema (parent Id
|
# from our ORDER BY, which matches insertion order for this schema (parent Id
|
||||||
@@ -175,6 +193,16 @@ def dump(args):
|
|||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
out.append("-- " + table + " (" + str(len(rows)) + " rows)")
|
out.append("-- " + table + " (" + str(len(rows)) + " rows)")
|
||||||
|
|
||||||
|
# Columns encrypted at rest cannot be dumped verbatim; emit NULL and
|
||||||
|
# note it so the secret value is restored through the app afterwards.
|
||||||
|
nulled = [c for c in columns if (table, c) in ENCRYPTED_COLUMNS]
|
||||||
|
for c in nulled:
|
||||||
|
out.append(
|
||||||
|
"-- NOTE: [{}] is an encrypted secret column — dumped as NULL. "
|
||||||
|
"Restore via the app (CLI/API) post-seed.".format(c)
|
||||||
|
)
|
||||||
|
|
||||||
if not rows:
|
if not rows:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -183,7 +211,10 @@ def dump(args):
|
|||||||
if identity:
|
if identity:
|
||||||
out.append("SET IDENTITY_INSERT [{}] ON;".format(table))
|
out.append("SET IDENTITY_INSERT [{}] ON;".format(table))
|
||||||
for row in rows:
|
for row in rows:
|
||||||
values = ", ".join(quote(v) for v in row)
|
values = ", ".join(
|
||||||
|
"NULL" if (table, c) in ENCRYPTED_COLUMNS else quote(v)
|
||||||
|
for c, v in zip(columns, row)
|
||||||
|
)
|
||||||
out.append(
|
out.append(
|
||||||
"INSERT INTO [{}] ({}) VALUES ({});".format(table, col_list, values)
|
"INSERT INTO [{}] ({}) VALUES ({});".format(table, col_list, values)
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user