Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/pr-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ jobs:
run: |
docker build --network=host -f src/DonkeyWork.CodeSandbox.Server/Dockerfile -t donkeywork-codesandbox-server:test .
docker build --network=host -f src/DonkeyWork.CodeSandbox.McpServer/Dockerfile -t donkeywork-codesandbox-mcpserver:test .
docker build --network=host -f src/DonkeyWork.CodeSandbox.AuthProxy/Dockerfile -t donkeywork-codesandbox-authproxy:test .

- name: Restore dependencies
run: dotnet restore DonkeyWork.CodeSandbox.sln
Expand Down Expand Up @@ -144,6 +145,9 @@ jobs:
- name: mcp-server
dockerfile: ./src/DonkeyWork.CodeSandbox.McpServer/Dockerfile
context: .
- name: authproxy
dockerfile: ./src/DonkeyWork.CodeSandbox.AuthProxy/Dockerfile
context: .
- name: frontend
dockerfile: ./frontend/Dockerfile
context: ./frontend
Expand Down Expand Up @@ -210,8 +214,6 @@ jobs:
run: dotnet format DonkeyWork.CodeSandbox.sln --verify-no-changes --verbosity diagnostic
continue-on-error: true

- name: Security scan - Dependency check
run: dotnet list package --vulnerable --include-transitive

pr-status:
name: PR Status
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ jobs:
run: |
docker build --network=host -f src/DonkeyWork.CodeSandbox.Server/Dockerfile -t donkeywork-codesandbox-server:test .
docker build --network=host -f src/DonkeyWork.CodeSandbox.McpServer/Dockerfile -t donkeywork-codesandbox-mcpserver:test .
docker build --network=host -f src/DonkeyWork.CodeSandbox.AuthProxy/Dockerfile -t donkeywork-codesandbox-authproxy:test .

- name: Restore dependencies
run: dotnet restore DonkeyWork.CodeSandbox.sln
Expand Down Expand Up @@ -159,6 +160,12 @@ jobs:
context: .
title: DonkeyWork CodeSandbox MCP Server
description: MCP bridge server for AI agent integration
- component: authproxy
image: donkeywork-codesandbox-authproxy
dockerfile: ./src/DonkeyWork.CodeSandbox.AuthProxy/Dockerfile
context: .
title: DonkeyWork CodeSandbox Auth Proxy
description: TLS MITM forward proxy sidecar for credential injection
- component: frontend
image: donkeywork-codesandbox-frontend
dockerfile: ./frontend/Dockerfile
Expand Down Expand Up @@ -260,6 +267,7 @@ jobs:
echo -e "### Manager API\n\`\`\`bash\ndocker pull ${{ env.REGISTRY }}/${{ github.repository_owner }}/donkeywork-codesandbox-manager:${VERSION}\n\`\`\`\n" >> CHANGELOG.md
echo -e "### Executor\n\`\`\`bash\ndocker pull ${{ env.REGISTRY }}/${{ github.repository_owner }}/donkeywork-codesandbox-executor:${VERSION}\n\`\`\`\n" >> CHANGELOG.md
echo -e "### MCP Server\n\`\`\`bash\ndocker pull ${{ env.REGISTRY }}/${{ github.repository_owner }}/donkeywork-codesandbox-mcp-server:${VERSION}\n\`\`\`\n" >> CHANGELOG.md
echo -e "### Auth Proxy\n\`\`\`bash\ndocker pull ${{ env.REGISTRY }}/${{ github.repository_owner }}/donkeywork-codesandbox-authproxy:${VERSION}\n\`\`\`\n" >> CHANGELOG.md
echo -e "### Frontend\n\`\`\`bash\ndocker pull ${{ env.REGISTRY }}/${{ github.repository_owner }}/donkeywork-codesandbox-frontend:${VERSION}\n\`\`\`\n" >> CHANGELOG.md

echo "**Platforms:** linux/amd64, linux/arm64" >> CHANGELOG.md
Expand Down Expand Up @@ -293,6 +301,7 @@ jobs:
echo "| Manager | ${{ needs.docker-build.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Executor | ${{ needs.docker-build.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| MCP Server | ${{ needs.docker-build.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Auth Proxy | ${{ needs.docker-build.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Frontend | ${{ needs.docker-build.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| GitHub Release | ${{ needs.create-release.result }} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
Expand All @@ -301,5 +310,6 @@ jobs:
echo "docker pull ${{ env.REGISTRY }}/${{ github.repository_owner }}/donkeywork-codesandbox-manager:${VERSION}" >> $GITHUB_STEP_SUMMARY
echo "docker pull ${{ env.REGISTRY }}/${{ github.repository_owner }}/donkeywork-codesandbox-executor:${VERSION}" >> $GITHUB_STEP_SUMMARY
echo "docker pull ${{ env.REGISTRY }}/${{ github.repository_owner }}/donkeywork-codesandbox-mcp-server:${VERSION}" >> $GITHUB_STEP_SUMMARY
echo "docker pull ${{ env.REGISTRY }}/${{ github.repository_owner }}/donkeywork-codesandbox-authproxy:${VERSION}" >> $GITHUB_STEP_SUMMARY
echo "docker pull ${{ env.REGISTRY }}/${{ github.repository_owner }}/donkeywork-codesandbox-frontend:${VERSION}" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
30 changes: 30 additions & 0 deletions DonkeyWork.CodeSandbox.sln
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DonkeyWork.CodeSandbox.McpS
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DonkeyWork.CodeSandbox.McpServer.IntegrationTests", "test\DonkeyWork.CodeSandbox.McpServer.IntegrationTests\DonkeyWork.CodeSandbox.McpServer.IntegrationTests.csproj", "{D2E3F4A5-6B7C-8D9E-0F1A-2B3C4D5E6F7A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DonkeyWork.CodeSandbox.AuthProxy", "src\DonkeyWork.CodeSandbox.AuthProxy\DonkeyWork.CodeSandbox.AuthProxy.csproj", "{E3F4A5B6-7C8D-9E0F-1A2B-3C4D5E6F7A8B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DonkeyWork.CodeSandbox.AuthProxy.Tests", "test\DonkeyWork.CodeSandbox.AuthProxy.Tests\DonkeyWork.CodeSandbox.AuthProxy.Tests.csproj", "{F4A5B6C7-8D9E-0F1A-2B3C-4D5E6F7A8B9C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -126,6 +130,30 @@ Global
{D2E3F4A5-6B7C-8D9E-0F1A-2B3C4D5E6F7A}.Release|x64.Build.0 = Release|Any CPU
{D2E3F4A5-6B7C-8D9E-0F1A-2B3C4D5E6F7A}.Release|x86.ActiveCfg = Release|Any CPU
{D2E3F4A5-6B7C-8D9E-0F1A-2B3C4D5E6F7A}.Release|x86.Build.0 = Release|Any CPU
{E3F4A5B6-7C8D-9E0F-1A2B-3C4D5E6F7A8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E3F4A5B6-7C8D-9E0F-1A2B-3C4D5E6F7A8B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E3F4A5B6-7C8D-9E0F-1A2B-3C4D5E6F7A8B}.Debug|x64.ActiveCfg = Debug|Any CPU
{E3F4A5B6-7C8D-9E0F-1A2B-3C4D5E6F7A8B}.Debug|x64.Build.0 = Debug|Any CPU
{E3F4A5B6-7C8D-9E0F-1A2B-3C4D5E6F7A8B}.Debug|x86.ActiveCfg = Debug|Any CPU
{E3F4A5B6-7C8D-9E0F-1A2B-3C4D5E6F7A8B}.Debug|x86.Build.0 = Debug|Any CPU
{E3F4A5B6-7C8D-9E0F-1A2B-3C4D5E6F7A8B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E3F4A5B6-7C8D-9E0F-1A2B-3C4D5E6F7A8B}.Release|Any CPU.Build.0 = Release|Any CPU
{E3F4A5B6-7C8D-9E0F-1A2B-3C4D5E6F7A8B}.Release|x64.ActiveCfg = Release|Any CPU
{E3F4A5B6-7C8D-9E0F-1A2B-3C4D5E6F7A8B}.Release|x64.Build.0 = Release|Any CPU
{E3F4A5B6-7C8D-9E0F-1A2B-3C4D5E6F7A8B}.Release|x86.ActiveCfg = Release|Any CPU
{E3F4A5B6-7C8D-9E0F-1A2B-3C4D5E6F7A8B}.Release|x86.Build.0 = Release|Any CPU
{F4A5B6C7-8D9E-0F1A-2B3C-4D5E6F7A8B9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F4A5B6C7-8D9E-0F1A-2B3C-4D5E6F7A8B9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F4A5B6C7-8D9E-0F1A-2B3C-4D5E6F7A8B9C}.Debug|x64.ActiveCfg = Debug|Any CPU
{F4A5B6C7-8D9E-0F1A-2B3C-4D5E6F7A8B9C}.Debug|x64.Build.0 = Debug|Any CPU
{F4A5B6C7-8D9E-0F1A-2B3C-4D5E6F7A8B9C}.Debug|x86.ActiveCfg = Debug|Any CPU
{F4A5B6C7-8D9E-0F1A-2B3C-4D5E6F7A8B9C}.Debug|x86.Build.0 = Debug|Any CPU
{F4A5B6C7-8D9E-0F1A-2B3C-4D5E6F7A8B9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F4A5B6C7-8D9E-0F1A-2B3C-4D5E6F7A8B9C}.Release|Any CPU.Build.0 = Release|Any CPU
{F4A5B6C7-8D9E-0F1A-2B3C-4D5E6F7A8B9C}.Release|x64.ActiveCfg = Release|Any CPU
{F4A5B6C7-8D9E-0F1A-2B3C-4D5E6F7A8B9C}.Release|x64.Build.0 = Release|Any CPU
{F4A5B6C7-8D9E-0F1A-2B3C-4D5E6F7A8B9C}.Release|x86.ActiveCfg = Release|Any CPU
{F4A5B6C7-8D9E-0F1A-2B3C-4D5E6F7A8B9C}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -139,5 +167,7 @@ Global
{60647C45-AAAF-410D-91BF-0414803E8B58} = {E8B1F4C1-9A2B-4E3D-8F7A-1D3C5E6F7A8B}
{B1F2E3D4-5A6B-7C8D-9E0F-1A2B3C4D5E6F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{D2E3F4A5-6B7C-8D9E-0F1A-2B3C4D5E6F7A} = {E8B1F4C1-9A2B-4E3D-8F7A-1D3C5E6F7A8B}
{E3F4A5B6-7C8D-9E0F-1A2B-3C4D5E6F7A8B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{F4A5B6C7-8D9E-0F1A-2B3C-4D5E6F7A8B9C} = {E8B1F4C1-9A2B-4E3D-8F7A-1D3C5E6F7A8B}
EndGlobalSection
EndGlobal
28 changes: 28 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,34 @@ services:
networks:
- kata-network

auth-proxy:
build:
context: .
dockerfile: src/DonkeyWork.CodeSandbox.AuthProxy/Dockerfile
container_name: auth-proxy
ports:
- "8080:8080"
- "8081:8081"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ProxyConfiguration__ProxyPort=8080
- ProxyConfiguration__HealthPort=8081
- ProxyConfiguration__AllowedDomains__0=httpbin.org
- ProxyConfiguration__AllowedDomains__1=graph.microsoft.com
- ProxyConfiguration__AllowedDomains__2=api.github.com
- ProxyConfiguration__AllowedDomains__3=github.com
- ProxyConfiguration__CaCertificatePath=/certs/ca.crt
- ProxyConfiguration__CaPrivateKeyPath=/certs/ca.key
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8081/healthz"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
networks:
- kata-network

frontend:
build:
context: ./frontend
Expand Down
39 changes: 39 additions & 0 deletions scripts/generate-ca.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/bin/bash
# generate-ca.sh — Generate the internal CA certificate and key for the auth proxy sidecar.
#
# Usage:
# ./scripts/generate-ca.sh [OUTPUT_DIR] [VALIDITY_DAYS]
#
# Examples:
# ./scripts/generate-ca.sh # Output to current dir, 365 days
# ./scripts/generate-ca.sh ./certs 730 # Output to ./certs, 2 years
#
# The generated files:
# ca.crt — PEM-encoded CA certificate (install in trust stores)
# ca.key — PEM-encoded CA private key (mount only into sidecar)

set -euo pipefail

OUTPUT_DIR="${1:-.}"
VALIDITY_DAYS="${2:-365}"
SUBJECT="/CN=DonkeyWork CodeSandbox Internal CA/O=DonkeyWork/OU=CodeSandbox"

mkdir -p "$OUTPUT_DIR"

openssl req -x509 -newkey rsa:4096 \
-keyout "$OUTPUT_DIR/ca.key" \
-out "$OUTPUT_DIR/ca.crt" \
-sha256 \
-days "$VALIDITY_DAYS" \
-nodes \
-subj "$SUBJECT" \
-addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
-addext "keyUsage=critical,keyCertSign,cRLSign"

echo ""
echo "CA certificate generated successfully:"
echo " Certificate: $OUTPUT_DIR/ca.crt"
echo " Private key: $OUTPUT_DIR/ca.key"
echo " Validity: $VALIDITY_DAYS days"
echo ""
echo "To inspect: openssl x509 -in $OUTPUT_DIR/ca.crt -text -noout"
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;

namespace DonkeyWork.CodeSandbox.AuthProxy.Configuration;

public class ProxyConfiguration
{
[Range(1, 65535)]
public int ProxyPort { get; set; } = 8080;

[Range(1, 65535)]
public int HealthPort { get; set; } = 8081;

public List<string> AllowedDomains { get; set; } = new();

public string CaCertificatePath { get; set; } = "/certs/ca.crt";

public string CaPrivateKeyPath { get; set; } = "/certs/ca.key";
}
48 changes: 48 additions & 0 deletions src/DonkeyWork.CodeSandbox.AuthProxy/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src

# Copy Directory.Packages.props for central package management
COPY ["Directory.Packages.props", "./"]

# Copy nuget.config if present (for CI with custom package source)
COPY nuget.config* ./

# Copy project file and restore
COPY ["src/DonkeyWork.CodeSandbox.AuthProxy/DonkeyWork.CodeSandbox.AuthProxy.csproj", "DonkeyWork.CodeSandbox.AuthProxy/"]
RUN dotnet restore "DonkeyWork.CodeSandbox.AuthProxy/DonkeyWork.CodeSandbox.AuthProxy.csproj"

# Copy source and build
COPY ["src/", "."]
WORKDIR "/src/DonkeyWork.CodeSandbox.AuthProxy"
RUN dotnet build "DonkeyWork.CodeSandbox.AuthProxy.csproj" -c Release -o /app/build

# Publish stage
FROM build AS publish
RUN dotnet publish "DonkeyWork.CodeSandbox.AuthProxy.csproj" -c Release -o /app/publish /p:UseAppHost=false

# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app

# Create non-root user
RUN groupadd --system --gid 10000 appuser \
&& useradd --system --uid 10000 --gid 10000 --home-dir /app appuser

# Copy published app
COPY --from=publish /app/publish .

# Change ownership to non-root user
RUN chown -R appuser:appuser /app

# Switch to non-root user
USER appuser

EXPOSE 8080 8081

ENV ASPNETCORE_ENVIRONMENT=Production

HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD curl -f http://localhost:8081/healthz || exit 1

ENTRYPOINT ["dotnet", "DonkeyWork.CodeSandbox.AuthProxy.dll"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Serilog.AspNetCore" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="DonkeyWork.CodeSandbox.AuthProxy.Tests" />
</ItemGroup>

</Project>
10 changes: 10 additions & 0 deletions src/DonkeyWork.CodeSandbox.AuthProxy/Health/HealthEndpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace DonkeyWork.CodeSandbox.AuthProxy.Health;

public static class HealthEndpoint
{
public static WebApplication MapHealthEndpoints(this WebApplication app)
{
app.MapGet("/healthz", () => Results.Ok(new { status = "healthy" }));
return app;
}
}
50 changes: 50 additions & 0 deletions src/DonkeyWork.CodeSandbox.AuthProxy/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using DonkeyWork.CodeSandbox.AuthProxy.Configuration;
using DonkeyWork.CodeSandbox.AuthProxy.Health;
using DonkeyWork.CodeSandbox.AuthProxy.Proxy;
using Serilog;

var builder = WebApplication.CreateBuilder(args);

// Add Serilog
builder.Host.UseSerilog((context, services, configuration) => configuration
.ReadFrom.Configuration(context.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext());

Log.Information("Starting Auth Proxy Sidecar");

// Bind configuration
var proxyConfig = new ProxyConfiguration();
builder.Configuration.GetSection(nameof(ProxyConfiguration)).Bind(proxyConfig);
builder.Services.AddSingleton(proxyConfig);

// Configure Kestrel to listen on the health port
builder.WebHost.UseUrls($"http://0.0.0.0:{proxyConfig.HealthPort}");

// Load or generate CA certificate
using var loggerFactory = LoggerFactory.Create(lb => lb.AddSerilog(Log.Logger));
var startupLogger = loggerFactory.CreateLogger("AuthProxy.Startup");
var caCert = CertificateGenerator.LoadOrGenerateCaCertificate(
proxyConfig.CaCertificatePath,
proxyConfig.CaPrivateKeyPath,
startupLogger);

// Register services
builder.Services.AddSingleton(sp => new CertificateGenerator(
caCert,
sp.GetRequiredService<ILogger<CertificateGenerator>>()));

builder.Services.AddSingleton<TlsMitmHandler>();
builder.Services.AddHostedService<ProxyServer>();
builder.Services.AddHealthChecks();

var app = builder.Build();

// Map health endpoints
app.MapHealthEndpoints();
app.MapHealthChecks("/health");

Log.Information("Auth Proxy configured: proxy port {ProxyPort}, health port {HealthPort}, allowed domains: {Domains}",
proxyConfig.ProxyPort, proxyConfig.HealthPort, string.Join(", ", proxyConfig.AllowedDomains));

await app.RunAsync();
Loading
Loading