Skip to content

Commit

Permalink
Configure HTTPS health probes and secure internal health endpoints (#521
Browse files Browse the repository at this point in the history
)

### Summary & Motivation

Configure Azure Container Apps HTTPS health probes for the Account
Management and Back Office API and Workers for Live and Readiness health
checks. AppGateway will continue using TCP health probes.

Initially, Azure Container App uses a QuickStart Image that lacks HTTP
health endpoints. Therefore, HTTP health endpoints are configured only
after infrastructure deployment when a self-contained system with health
endpoints is deployed.

Relocate health endpoints to `[self-contained-system]/internal-api/live`
and `[self-contained-system]/internal-api/readiness`.

Create a new `RequestTransformation` in YARP AppGateway to block all
traffic to any path containing `/internal-api/`, ensuring that health
endpoints are not publicly accessible but can be only accessed
internally by Azure Container Apps.


### Checklist

- [x] I have added a Label to the pull-request
- [x] I have added tests, and done manual regression tests
- [x] I have updated the documentation, if necessary
  • Loading branch information
tjementum authored Jul 1, 2024
2 parents 541ffd0 + cd5799a commit 9d95b23
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 4 deletions.
5 changes: 5 additions & 0 deletions application/AppGateway/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
);
}

builder.Services.AddSingleton<BlockInternalApiTransform>();
reverseProxyBuilder.AddTransforms(context =>
context.RequestTransforms.Add(context.Services.GetRequiredService<BlockInternalApiTransform>())
);

builder.Services.AddNamedBlobStorages(builder, ("avatars-storage", "AVATARS_STORAGE_URL"));

builder.WebHost.UseKestrel(option => option.AddServerHeader = false);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Yarp.ReverseProxy.Transforms;

namespace PlatformPlatform.AppGateway.Transformations;

public class BlockInternalApiTransform : RequestTransform
{
public override async ValueTask ApplyAsync(RequestTransformContext context)
{
if (context.HttpContext.Request.Path.Value?.Contains("/internal-api/", StringComparison.OrdinalIgnoreCase) == true)
{
context.HttpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
context.HttpContext.Response.ContentType = "text/plain";
await context.HttpContext.Response.WriteAsync("Access to internal API is forbidden.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ public class ManagedIdentityTransform(TokenCredential credential)
{
protected override string? GetValue(RequestTransformContext context)
{
if (!context.HttpContext.Request.Path.StartsWithSegments("/avatars")) return null;
if (!context.HttpContext.Request.Path.StartsWithSegments("/avatars", StringComparison.OrdinalIgnoreCase))
{
return null;
}

var tokenRequestContext = new TokenRequestContext(["https://storage.azure.com/.default"]);
var token = credential.GetToken(tokenRequestContext, context.HttpContext.RequestAborted);
Expand Down
2 changes: 1 addition & 1 deletion application/AppGateway/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"account-management-api": {
"ClusterId": "account-management-api",
"Match": {
"Path": "/api/{**catch-all}"
"Path": "/api/account-management-api/{**catch-all}"
},
"Transforms": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ public class HealthEndpoints : IEndpoints
public void MapEndpoints(IEndpointRouteBuilder routes)
{
// All health checks must pass for app to be considered ready to accept traffic after starting
routes.MapHealthChecks("/health");
routes.MapHealthChecks("/internal-api/ready");

// Only health checks tagged with the "live" tag must pass for app to be considered alive
routes.MapHealthChecks("/alive", new HealthCheckOptions { Predicate = r => r.Tags.Contains("live") });
routes.MapHealthChecks("/internal-api/live", new HealthCheckOptions { Predicate = r => r.Tags.Contains("live") });
}
}
5 changes: 5 additions & 0 deletions cloud-infrastructure/cluster/main-cluster.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ module accountManagementWorkers '../modules/container-app.bicep' = {
maxReplicas: 3
userAssignedIdentityName: accountManagementIdentityName
ingress: true
hasProbesEndpoint: true
environmentVariables: accountManagementEnvironmentVariables
}
dependsOn: [accountManagementDatabase, accountManagementIdentity, communicationService]
Expand All @@ -257,6 +258,7 @@ module accountManagementApi '../modules/container-app.bicep' = {
maxReplicas: 3
userAssignedIdentityName: accountManagementIdentityName
ingress: true
hasProbesEndpoint: true
environmentVariables: accountManagementEnvironmentVariables
}
dependsOn: [accountManagementDatabase, accountManagementIdentity, communicationService, accountManagementWorkers]
Expand Down Expand Up @@ -358,6 +360,7 @@ module backOfficeWorkers '../modules/container-app.bicep' = {
maxReplicas: 1
userAssignedIdentityName: backOfficeIdentityName
ingress: true
hasProbesEndpoint: true
environmentVariables: backOfficeEnvironmentVariables
}
dependsOn: [backOfficeDatabase, backOfficeIdentity, communicationService]
Expand All @@ -382,6 +385,7 @@ module backOfficeApi '../modules/container-app.bicep' = {
maxReplicas: 1
userAssignedIdentityName: backOfficeIdentityName
ingress: true
hasProbesEndpoint: true
environmentVariables: backOfficeEnvironmentVariables
}
dependsOn: [backOfficeDatabase, backOfficeIdentity, communicationService, backOfficeWorkers]
Expand Down Expand Up @@ -423,6 +427,7 @@ module appGateway '../modules/container-app.bicep' = {
maxReplicas: 3
userAssignedIdentityName: appGatewayIdentityName
ingress: true
hasProbesEndpoint: false
domainName: domainName == '' ? '' : domainName
isDomainConfigured: domainName != '' && isDomainConfigured
external: true
Expand Down
53 changes: 53 additions & 0 deletions cloud-infrastructure/modules/container-app.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ param minReplicas int = 1
param maxReplicas int = 3
param userAssignedIdentityName string
param ingress bool
param hasProbesEndpoint bool
param domainName string = ''
param isDomainConfigured bool = false
param external bool = false
Expand Down Expand Up @@ -93,6 +94,58 @@ resource containerApp 'Microsoft.App/containerApps@2023-05-02-preview' = {
memory: memory
}
env: environmentVariables
probes: hasProbesEndpoint && containerImageTag != 'initial' // The quickstart image does not have liveness and readiness probes
? [
{
type: 'Liveness'
httpGet: {
path: '/internal-api/live'
port: 8080
scheme: 'HTTPS'
}
initialDelaySeconds: 3
failureThreshold: 3
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 1
}
{
type: 'Readiness'
httpGet: {
path: '/internal-api/ready'
port: 8080
scheme: 'HTTPS'
}
initialDelaySeconds: 3
failureThreshold: 3
periodSeconds: 6
successThreshold: 1
timeoutSeconds: 5
}
]
: [
{
type: 'liveness'
failureThreshold: 3
periodSeconds: 10
successThreshold: 1
tcpSocket: {
port: 8080
}
timeoutSeconds: 1
}
{
type: 'readiness'
failureThreshold: 48
initialDelaySeconds: 3
periodSeconds: 5
successThreshold: 1
tcpSocket: {
port: 8080
}
timeoutSeconds: 5
}
]
}
]
revisionSuffix: revisionSuffix
Expand Down

0 comments on commit 9d95b23

Please sign in to comment.