Docker sandbox credential injection for Business Central
I recently blogged about how to use Docker Sandboxes and Kits to work with the AL MCP Server and the Azure DevOps MCP Server from GitHub Copilot in a secure way. Maybe you noticed, I showed how to inject the credentials for the Azure DevOps MCP Server, but not for the AL MCP Server, which actually is the Microsoft D365 Busines Central authentication. With a lot of help by Claude Code and the phantastic team at Docker, I figured out how to do that1 and want to share it with you as well.
The TL;DR
Let me start by showing you the result:
As you can see, I never had to authenticate within the sandbox, because the credentials are injected by the proxy on the host. If you want to know how that works, join the ride.
The details: Injecting the credentials on the Sandbox side
On the Sandbox side, this looks very similar to what I have shown in the previous blog post for the Azure DevOps MCP Server:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
schemaVersion: "1"
kind: mixin
name: al
displayName: al
description: altool installation
network:
allowedDomains:
- dot.net
- ci.dot.net
- builds.dotnet.microsoft.com
- api.nuget.org
- fps-alpaca.westeurope.cloudapp.azure.com
serviceDomains:
fps-alpaca.westeurope.cloudapp.azure.com: bc
serviceAuth:
bc:
headerName: Authorization
valueFormat: "Basic %s"
credentials:
sources:
bc:
env:
- BC
commands:
install:
- command: "curl -fsSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 8.0 && /home/agent/.dotnet/dotnet tool install --global Microsoft.Dynamics.BusinessCentral.Development.Tools"
user: "1000"
description: Install altool
startup:
- command: ["/bin/sh", "/home/agent/patch-al-crl.sh"]
environment:
proxyManaged:
- BC
My Business Central environment is a COSMO Alpaca container available at fps-alpaca.westeurope.cloudapp.azure.com, so I first need to give the sandbox access to that URL (line 13). Then the serviceDomains and serviceAuth in lines 14-19 define that we also want to inject credentials, which in turn are set up in lines 21-25. And we need to let the proxy know that it should manage those credenitals in lines 35-37.
Now all that is left is to define the secret bc on the host containing the actual credentials
1
echo "$([System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes("tfenster:Super5ecret!")))" | sbx secret set -g bc
Or at least I thought so initially. But if it would have been that easy, I would have shared it last time already. So why didn’t this work?
The details: The problem with basic auth / UserPassword
First, the container is set up with UserPassword aka basic authentication. But if the agent calls al_* commands requiring server auth like downloading symbols, then it always wants to get a username and password, which a) would be insecure and b) doesn’t work in this setup anyway. The other options are Azure Entra ID / AAD or NTLM / Windows authentication. I initially thought both would not be an option because, well, the container just isn’t configured that way. And for AAD, that is actually true. But Windows coincidentally does exactly what we need: It just calls the server without asking for anything. Of course, there is no Windows / NTLM user in our Linux sandbox, so without the proxy this wouldn’t work, but because the proxy injects the correct credentials, that is no problem.
Great, right? Unfortunately, not quite.
The details: The problem with Windows auth
The second problem is that if you use Windows auth, the AL MCP Server always strictly validates the Certificate Revocation List when making the connection to the server and the certificate that the Sandbox proxy injects doesn’t provide one. Now I could try to appear clever and claim how I figured that out and created a workaround, but credit where credit is due: Claude Code with Opus 4.6 did that… Here is the actual response after a lengthy bit of analyzing and some prodding by myself into the right direction
1
2
3
- The proxy does MITM SSL inspection, and the AL tool's Windows auth code path explicitly sets CheckCertificateRevocationList = true
- The proxy's MITM cert has no CRL endpoint, so the revocation check always failed
- Fix: patched Microsoft.Dynamics.Nav.Deployment.dll with Mono.Cecil to change that single CheckCertificateRevocationList = true call to false
There is a miniscule chance that I might have figured out the actual SSL issue as there always was just a generic “SSL connection failed” error message. And there is a chance of exactly 0% that I would have been able to come up with the following script that fixes it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#!/bin/bash
set -e
# Patch Microsoft.Dynamics.Nav.Deployment.dll to disable CheckCertificateRevocationList.
# This is needed in sandbox environments where the proxy does MITM SSL inspection
# but the proxy cert has no CRL endpoint - causing Windows auth symbol downloads to fail.
DOTNET="${DOTNET_ROOT:-$HOME/.dotnet}/dotnet"
STORE="$HOME/.dotnet/tools/.store/microsoft.dynamics.businesscentral.development.tools"
# Find the DLL (handles any installed version)
DLL=$(find "$STORE" -name "Microsoft.Dynamics.Nav.Deployment.dll" 2>/dev/null | head -1)
if [ -z "$DLL" ]; then
echo "ERROR: Microsoft.Dynamics.Nav.Deployment.dll not found. Is the AL tool installed?"
exit 1
fi
echo "Found: $DLL"
# Skip if already patched (backup exists)
if [ -f "$DLL.bak" ]; then
echo "Already patched (backup found at $DLL.bak). Skipping."
exit 0
fi
# Build the patcher
PATCHER_DIR=$(mktemp -d)
trap "rm -rf $PATCHER_DIR" EXIT
cat > "$PATCHER_DIR/patcher.csproj" << 'EOF'
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Mono.Cecil" Version="0.11.4" />
</ItemGroup>
</Project>
EOF
cat > "$PATCHER_DIR/Program.cs" << 'EOF'
using Mono.Cecil;
using Mono.Cecil.Cil;
using System;
using System.Linq;
var dllPath = args[0];
Console.WriteLine($"Patching: {dllPath}");
var asm = AssemblyDefinition.ReadAssembly(dllPath, new ReaderParameters { ReadWrite = true });
int patched = 0;
foreach (var type in asm.MainModule.Types)
foreach (var method in type.Methods.Where(m => m.HasBody)) {
var instrs = method.Body.Instructions;
for (int i = 1; i < instrs.Count; i++) {
var instr = instrs[i];
if ((instr.OpCode == OpCodes.Callvirt || instr.OpCode == OpCodes.Call)
&& instr.Operand is MethodReference mr
&& mr.Name == "set_CheckCertificateRevocationList") {
var prev = instrs[i-1];
if (prev.OpCode == OpCodes.Ldc_I4_1
|| (prev.OpCode == OpCodes.Ldc_I4 && (int)prev.Operand == 1)) {
prev.OpCode = OpCodes.Ldc_I4_0;
Console.WriteLine($" Patched in {method.FullName}");
patched++;
}
}
}
}
if (patched == 0) {
Console.WriteLine("No patch sites found - already patched or DLL structure changed.");
Environment.Exit(2);
}
asm.Write();
Console.WriteLine($"Done ({patched} patch(es) applied).");
EOF
echo "Building patcher..."
cd "$PATCHER_DIR"
"$DOTNET" build -o "$PATCHER_DIR/out" -v q 2>&1 | tail -3
echo "Applying patch..."
cp "$DLL" "$DLL.bak"
chmod u+w "$DLL"
"$DOTNET" "$PATCHER_DIR/out/patcher.dll" "$DLL"
echo "Patch applied. Restart the AL MCP server to pick up the change."
After Claude came up with this beauty2, I could put this script into files/home/patch-al-crl.sh in my kit folder. That puts the script into the home folder of the agent, so that I could reference it in my kit in the startup commands, see lines 32/33 in the spec above. And that is also the reason why you see me stop and start the AL MCP Server in the screen recording as very first step.
I’ll try to work with both the Docker and the BC team to potentially come up with a better solution, but I am quite happy that it works and also seems to do so in a stable way!
Webmentions:
No webmentions were found.
No likes were found.
No reposts were found.
.png)