Skip to content

Commit 46c93a2

Browse files
feat: add trust store management for upstream TLS verification
Trust stores hold CA certificate bundles used to verify backend servers when the proxy connects to upstreams over HTTPS, completing Phase 8.1 (TLS / Certificate Management). Includes schema with PEM parsing/validation, Services context CRUD, API controller, LiveView pages (index/new/show/edit), KDL generation (trust_stores block + upstream group TLS verify block), upstream group form integration, and 41 new tests.
1 parent f27f233 commit 46c93a2

19 files changed

Lines changed: 1739 additions & 13 deletions

File tree

lib/sentinel_cp/services.ex

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ defmodule SentinelCp.Services do
88

99
import Ecto.Query, warn: false
1010
alias SentinelCp.Repo
11-
alias SentinelCp.Services.{Service, ServiceTemplate, ProjectConfig, UpstreamGroup, UpstreamTarget, Certificate, AuthPolicy, OpenApiSpec, DiscoverySource, DiscoverySync, Middleware, ServiceMiddleware}
11+
alias SentinelCp.Services.{Service, ServiceTemplate, ProjectConfig, UpstreamGroup, UpstreamTarget, Certificate, TrustStore, AuthPolicy, OpenApiSpec, DiscoverySource, DiscoverySync, Middleware, ServiceMiddleware}
1212
alias SentinelCp.Secrets
1313

1414
## Services
@@ -357,6 +357,54 @@ defmodule SentinelCp.Services do
357357
|> Repo.update()
358358
end
359359

360+
## Trust Stores
361+
362+
@doc """
363+
Lists trust stores for a project, ordered by name.
364+
"""
365+
def list_trust_stores(project_id) do
366+
from(t in TrustStore,
367+
where: t.project_id == ^project_id,
368+
order_by: [asc: t.name]
369+
)
370+
|> Repo.all()
371+
end
372+
373+
@doc """
374+
Gets a single trust store by ID.
375+
"""
376+
def get_trust_store(id), do: Repo.get(TrustStore, id)
377+
378+
@doc """
379+
Gets a single trust store by ID, raises if not found.
380+
"""
381+
def get_trust_store!(id), do: Repo.get!(TrustStore, id)
382+
383+
@doc """
384+
Creates a trust store.
385+
"""
386+
def create_trust_store(attrs) do
387+
%TrustStore{}
388+
|> TrustStore.create_changeset(attrs)
389+
|> Repo.insert()
390+
end
391+
392+
@doc """
393+
Updates a trust store.
394+
"""
395+
def update_trust_store(%TrustStore{} = trust_store, attrs) do
396+
trust_store
397+
|> TrustStore.update_changeset(attrs)
398+
|> Repo.update()
399+
end
400+
401+
@doc """
402+
Deletes a trust store.
403+
"""
404+
def delete_trust_store(%TrustStore{} = trust_store) do
405+
Repo.delete(trust_store)
406+
end
407+
360408
## Service Templates
361409

362410
@doc """

lib/sentinel_cp/services/kdl_generator.ex

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ defmodule SentinelCp.Services.KdlGenerator do
66
"""
77

88
alias SentinelCp.Services
9-
alias SentinelCp.Services.{Service, ProjectConfig, UpstreamGroup, Certificate, AuthPolicy}
9+
alias SentinelCp.Services.{Service, ProjectConfig, UpstreamGroup, Certificate, TrustStore, AuthPolicy}
1010

1111
@doc """
1212
Generates KDL configuration for a project from its services and config.
@@ -29,6 +29,7 @@ defmodule SentinelCp.Services.KdlGenerator do
2929
upstream_groups = Services.list_upstream_groups(project_id)
3030
certificates = Services.list_certificates(project_id)
3131
auth_policies = Services.list_auth_policies(project_id)
32+
trust_stores = Services.list_trust_stores(project_id)
3233

3334
# Build middleware chain map: service_id -> [service_middlewares]
3435
middleware_chains =
@@ -40,7 +41,7 @@ defmodule SentinelCp.Services.KdlGenerator do
4041
# Resolve secret references if requested
4142
case maybe_resolve_secrets(services, opts) do
4243
{:ok, resolved_services} ->
43-
kdl = build_kdl(resolved_services, config, upstream_groups, certificates, auth_policies, middleware_chains)
44+
kdl = build_kdl(resolved_services, config, upstream_groups, certificates, auth_policies, middleware_chains, trust_stores)
4445
{:ok, kdl}
4546

4647
{:error, _} = error ->
@@ -92,7 +93,7 @@ defmodule SentinelCp.Services.KdlGenerator do
9293
Generates KDL from provided services, config, and upstream groups (no DB access).
9394
Useful for testing.
9495
"""
95-
def build_kdl(services, %ProjectConfig{} = config, upstream_groups \\ [], certificates \\ [], auth_policies \\ [], middleware_chains \\ %{}) do
96+
def build_kdl(services, %ProjectConfig{} = config, upstream_groups \\ [], certificates \\ [], auth_policies \\ [], middleware_chains \\ %{}, trust_stores \\ []) do
9697
# Build lookup maps
9798
group_map =
9899
upstream_groups
@@ -106,6 +107,10 @@ defmodule SentinelCp.Services.KdlGenerator do
106107
auth_policies
107108
|> Enum.into(%{}, fn a -> {a.id, a} end)
108109

110+
trust_store_map =
111+
trust_stores
112+
|> Enum.into(%{}, fn t -> {t.id, t} end)
113+
109114
# Determine which certificates are used by services
110115
used_cert_ids =
111116
services
@@ -115,14 +120,24 @@ defmodule SentinelCp.Services.KdlGenerator do
115120

116121
used_certs = Enum.filter(certificates, fn c -> MapSet.member?(used_cert_ids, c.id) end)
117122

123+
# Determine which trust stores are used by upstream groups
124+
used_trust_store_ids =
125+
upstream_groups
126+
|> Enum.map(& &1.trust_store_id)
127+
|> Enum.reject(&is_nil/1)
128+
|> MapSet.new()
129+
130+
used_trust_stores = Enum.filter(trust_stores, fn t -> MapSet.member?(used_trust_store_ids, t.id) end)
131+
118132
parts = [
119133
"// Generated by Sentinel Control Plane",
120134
"// Do not edit manually — managed via Services UI",
121135
"",
122136
build_settings(config),
123137
"",
124138
build_tls_certificates(used_certs),
125-
build_upstream_groups(upstream_groups),
139+
build_trust_stores(used_trust_stores),
140+
build_upstream_groups(upstream_groups, trust_store_map),
126141
build_routes(services, group_map, cert_map, auth_policy_map, middleware_chains),
127142
build_rate_limits(services)
128143
]
@@ -167,18 +182,38 @@ defmodule SentinelCp.Services.KdlGenerator do
167182
build_nested_map_block(config.default_security, "security", " ")
168183
end
169184

170-
defp build_upstream_groups([]), do: []
185+
defp build_trust_stores([]), do: []
171186

172-
defp build_upstream_groups(groups) do
187+
defp build_trust_stores(trust_stores) do
188+
blocks =
189+
trust_stores
190+
|> Enum.map(&build_trust_store_block/1)
191+
|> Enum.intersperse([""])
192+
193+
["trust_stores {"] ++ List.flatten(blocks) ++ ["}", ""]
194+
end
195+
196+
defp build_trust_store_block(%TrustStore{} = ts) do
197+
[
198+
" store #{inspect(ts.slug)} {",
199+
" ca_file \"/etc/sentinel/trust-stores/#{ts.slug}.pem\"",
200+
" }"
201+
]
202+
end
203+
204+
defp build_upstream_groups(groups, trust_store_map)
205+
defp build_upstream_groups([], _trust_store_map), do: []
206+
207+
defp build_upstream_groups(groups, trust_store_map) do
173208
blocks =
174209
groups
175-
|> Enum.map(&build_upstream_group_block/1)
210+
|> Enum.map(&build_upstream_group_block(&1, trust_store_map))
176211
|> Enum.intersperse([""])
177212

178213
["upstream_groups {"] ++ List.flatten(blocks) ++ ["}", ""]
179214
end
180215

181-
defp build_upstream_group_block(%UpstreamGroup{} = group) do
216+
defp build_upstream_group_block(%UpstreamGroup{} = group, trust_store_map) do
182217
lines = [" group #{inspect(group.slug)} {"]
183218
lines = lines ++ [" algorithm #{inspect(group.algorithm)}"]
184219

@@ -203,9 +238,29 @@ defmodule SentinelCp.Services.KdlGenerator do
203238
# Add sticky sessions
204239
lines = lines ++ build_nested_map_block(group.sticky_sessions, "sticky_sessions", " ")
205240

241+
# Add TLS verify block if trust store is linked
242+
lines = lines ++ build_upstream_tls_block(group.trust_store_id, trust_store_map)
243+
206244
lines ++ [" }"]
207245
end
208246

247+
defp build_upstream_tls_block(nil, _trust_store_map), do: []
248+
249+
defp build_upstream_tls_block(trust_store_id, trust_store_map) do
250+
case Map.get(trust_store_map, trust_store_id) do
251+
nil ->
252+
[]
253+
254+
ts ->
255+
[
256+
" tls {",
257+
" verify true",
258+
" ca_file \"/etc/sentinel/trust-stores/#{ts.slug}.pem\"",
259+
" }"
260+
]
261+
end
262+
end
263+
209264
defp build_routes(services, group_map, cert_map, auth_policy_map, middleware_chains) do
210265
route_blocks =
211266
services

0 commit comments

Comments
 (0)