Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
28 changes: 28 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,34 @@ ServiceFabricBackup/
*.ldf
*.ndf

# Local sample video staging (must not be committed)
/automotive-machine-vision/sample-video-local/

# Local Let's Encrypt artifacts (must not be committed)
/automotive-machine-vision/certs/live/
/automotive-machine-vision/certs/work/
/automotive-machine-vision/certs/logs/
/automotive-machine-vision/certs/*.pem
/automotive-machine-vision/certs/*.key
/automotive-machine-vision/certs/*.crt
/automotive-machine-vision/acr-config.json
/automotive-machine-vision/deployment-params.json
/automotive-machine-vision/deployment-template.json

# Local secrets and certificates
.env
.env.*
!.env.example
!.env.sample
!.env.template
*.pem
*.key
*.p12
*.crt
*.cer
*.secret
*.secrets

# Business Intelligence projects
*.rdl.data
*.bim.layout
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ Azure Container Instances with AMD SEV-SNP confidential computing:
- Side-by-side comparison mode (Confidential vs Standard SKU)
- Real-time encryption with SKR-released keys
- Live diagnostics and TEE hardware detection
- **[Automotive Machine Vision](/automotive-machine-vision/README.md)** 🆕 - Confidential ACI app with attestation-gated `.mp4` upload, HTTPS transport, CCE policy enforcement, and in-container redaction plus rounded overlays for cars, trucks, pedestrians, and street signs
- **[App + PostgreSQL Demo](/aci-samples/app-and-postgreSQL-demo/README.md)** - Basic confidential container app with hardware-enforced AMD SEV-SNP security and container policy, connected to a DCa/ECa AMD PostgreSQL Flexible Server with confidential compute protections
- Single-container Flask dashboard with 5,000 financial transactions
- Application Gateway (L7) → Private VNet → Confidential ACI → PostgreSQL (DCa/ECa AMD)
Expand Down
1 change: 1 addition & 0 deletions aci-samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Scripts and samples for creating Confidential Azure Container Instances (ACIs) u
| [Visual Attestation Demo](visual-attestation-demo/README.md) | Interactive web application demonstrating remote attestation via Microsoft Azure Attestation (MAA) using the SKR sidecar |
| [Visual Attestation Demo v2](visual-attestation-demo-v2/README.md) 🆕 | Simplified ACI port that calls MAA **directly** from the Flask app via the upstream `get-snp-report` tool (no SKR sidecar). Side-by-side Confidential vs Standard SKU deployment to demonstrate falsifiability. |
| [App + PostgreSQL Finance Demo](app-and-postgreSQL-demo/README.md) 🆕 | Confidential ACI with DCa/ECa AMD PostgreSQL, 5,000 financial transactions, Application Gateway, and 9 threat scenarios |
| [Automotive Machine Vision](../automotive-machine-vision/README.md) 🆕 | Attestation-gated HTTPS `.mp4` upload with consistent plate/sign redaction and rounded overlays for cars, trucks, pedestrians, and street signs under CCE policy enforcement |
| [Sealed Container](sealed-container/README.md) 🆕 | Production-shaped sealed container: hand-authored restrictive CCE policy (no exec, no stdio, no logging), SKR-released AES key that unwraps an encrypted data bundle into tmpfs inside the TEE, signed SBOM + Trivy scan + cosign signatures over every artifact, signed top-level manifest with checksums. |

## New in Sealed Container (June 2026)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
218 changes: 218 additions & 0 deletions automotive-machine-vision/Deploy-AutomotiveMachineVision.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
param(
[switch]$Build,
[switch]$Deploy,
[switch]$Cleanup,
[switch]$DeployAFD,
[string]$RegistryName,
[string]$ImageTag = "latest",
[ValidateRange(1, 30)][int]$CpuCores = 4,
[ValidateRange(2, 128)][int]$MemoryInGB = 6,
[ValidateRange(0, 30)][int]$ProcessingWorkers = 0,
[ValidateRange(1, 12)][int]$DetectEveryNFrames = 1,
[string]$TlsCertPath = "",
[string]$TlsKeyPath = ""
)

$ErrorActionPreference = "Stop"
$Location = "eastus"
$ImageName = "automotive-machine-vision"

function Invoke-Az {
param(
[string]$Description,
[scriptblock]$Command
)

& $Command
if ($LASTEXITCODE -ne 0) {
throw "$Description failed (exit code $LASTEXITCODE)."
}
}

function Invoke-AzValue {
param(
[string]$Description,
[scriptblock]$Command
)

$result = & $Command
if ($LASTEXITCODE -ne 0) {
throw "$Description failed (exit code $LASTEXITCODE)."
}

return $result
}

function Get-Config {
if (Test-Path "acr-config.json") { return Get-Content "acr-config.json" | ConvertFrom-Json }
return $null
}

function Save-Config($Config) {
$Config | ConvertTo-Json | Out-File -FilePath "acr-config.json" -Encoding UTF8
}

if ($Build) {
if (-not $RegistryName) {
$random = -join ((97..122) | Get-Random -Count 8 | ForEach-Object {[char]$_})
$RegistryName = "acr$random"
}

$ResourceGroup = "amv-$RegistryName-rg"
Invoke-Az "Create resource group" { az group create --name $ResourceGroup --location $Location | Out-Null }

Invoke-Az "Create Azure Container Registry" { az acr create --resource-group $ResourceGroup --name $RegistryName --sku Basic --admin-enabled true | Out-Null }
Invoke-Az "Build container image in ACR" { az acr build --registry $RegistryName --image "$ImageName`:$ImageTag" --file Dockerfile . }

$acrUsername = Invoke-AzValue "Fetch ACR username" { az acr credential show --name $RegistryName --query username -o tsv }
$acrPassword = Invoke-AzValue "Fetch ACR password" { az acr credential show --name $RegistryName --query "passwords[0].value" -o tsv }
$loginServer = Invoke-AzValue "Fetch ACR login server" { az acr show --name $RegistryName --query loginServer -o tsv }

Save-Config @{
registryName = $RegistryName
resourceGroup = $ResourceGroup
loginServer = $loginServer
imageName = $ImageName
imageTag = $ImageTag
fullImage = "$loginServer/$ImageName`:$ImageTag"
acrUsername = $acrUsername
acrPassword = $acrPassword
}

Write-Host "Build complete: $loginServer/$ImageName`:$ImageTag" -ForegroundColor Green
}

if ($Deploy) {
$config = Get-Config
if (-not $config) {
throw "Run -Build first (acr-config.json is missing)."
}

if ($ImageTag -and $ImageTag -ne "latest") {
$config.imageTag = $ImageTag
$config.fullImage = "$($config.loginServer)/$($config.imageName):$ImageTag"
}

$suffix = -join ((97..122) | Get-Random -Count 6 | ForEach-Object {[char]$_})
$dnsLabel = "amv-$suffix"
$containerName = "amv-container-$suffix"
$secret = [guid]::NewGuid().ToString()
$effectiveWorkers = if ($ProcessingWorkers -gt 0) { $ProcessingWorkers } else { [Math]::Max(6, [Math]::Min(24, $CpuCores)) }
$tlsCertPem = ""
$tlsKeyPem = ""

if (($TlsCertPath -and -not $TlsKeyPath) -or (-not $TlsCertPath -and $TlsKeyPath)) {
throw "To use a real TLS certificate, provide both -TlsCertPath and -TlsKeyPath."
}

if ($TlsCertPath -and $TlsKeyPath) {
if (-not (Test-Path $TlsCertPath)) {
throw "TLS certificate file not found: $TlsCertPath"
}
if (-not (Test-Path $TlsKeyPath)) {
throw "TLS private key file not found: $TlsKeyPath"
}

$tlsCertPem = Get-Content -Path $TlsCertPath -Raw
$tlsKeyPem = Get-Content -Path $TlsKeyPath -Raw

if ([string]::IsNullOrWhiteSpace($tlsCertPem) -or [string]::IsNullOrWhiteSpace($tlsKeyPem)) {
throw "TLS certificate and key files must not be empty."
}

Write-Host "Using provided CA-issued TLS certificate for deployment." -ForegroundColor Green
} else {
Write-Host "No TLS certificate files provided; deployment will fall back to self-signed certificate." -ForegroundColor Yellow
}

$params = @{
containerGroupName = @{ value = $containerName }
location = @{ value = $Location }
appImage = @{ value = $config.fullImage }
registryServer = @{ value = $config.loginServer }
registryUsername = @{ value = $config.acrUsername }
registryPassword = @{ value = $config.acrPassword }
dnsNameLabel = @{ value = $dnsLabel }
flaskSecretKey = @{ value = $secret }
tlsCertificatePem = @{ value = $tlsCertPem }
tlsPrivateKeyPem = @{ value = $tlsKeyPem }
cpuCores = @{ value = $CpuCores }
memoryInGB = @{ value = $MemoryInGB }
processingWorkers = @{ value = [string]$effectiveWorkers }
detectEveryNFrames = @{ value = [string]$DetectEveryNFrames }
}

$armParameterFile = @{
'$schema' = "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#"
contentVersion = "1.0.0.0"
parameters = $params
}

$armParameterFile | ConvertTo-Json -Depth 12 | Out-File -FilePath "deployment-params.json" -Encoding UTF8
Copy-Item -Path "deployment-template-original.json" -Destination "deployment-template.json" -Force

# az confcom pulls images via local Docker to compute measurements.
Invoke-Az "Authenticate Docker to ACR" { az acr login --name $config.registryName | Out-Null }
Invoke-Az "Pull latest image for policy generation" { docker pull $config.fullImage | Out-Null }

# CCE policy generation: binds policy to approved image and disables interactive access
Invoke-Az "Generate CCE policy" { az confcom acipolicygen -a deployment-template.json --parameters deployment-params.json --disable-stdio }

Invoke-Az "Deploy confidential container group $containerName" {
az deployment group create --resource-group $config.resourceGroup --template-file deployment-template.json --parameters '@deployment-params.json' containerGroupName=$containerName dnsNameLabel=$dnsLabel instanceName=$containerName | Out-Null
}

$fqdn = Invoke-AzValue "Get deployed container FQDN for $containerName" { az container show --resource-group $config.resourceGroup --name $containerName --query ipAddress.fqdn -o tsv }
if ([string]::IsNullOrWhiteSpace($fqdn)) {
throw "Deployment finished but no container FQDN was returned for $containerName."
}

if ($DeployAFD) {
Write-Host "Deploying Azure Front Door for HTTPS access..." -ForegroundColor Cyan
$afdDeploymentName = "deployment-afd-$(Get-Date -Format 'yyyyMMddHHmmss')"
Invoke-Az "Deploy Azure Front Door" {
az deployment group create `
--resource-group $config.resourceGroup `
--name $afdDeploymentName `
--template-file deployment-afd.bicep `
--parameters aciOriginFqdn=$fqdn `
| Out-Null
}

$afdEndpoint = Invoke-AzValue "Get AFD endpoint" {
az deployment group show `
--resource-group $config.resourceGroup `
--name $afdDeploymentName `
--query "properties.outputs.frontDoorEndpoint.value" -o tsv
}

Write-Host "Azure Front Door deployment complete!" -ForegroundColor Green
Write-Host "Endpoint (Azure-managed HTTPS):" -ForegroundColor Green
Write-Host " - https://$afdEndpoint" -ForegroundColor Green
Write-Host "" -ForegroundColor Green
Write-Host "Direct ACI endpoint (self-signed or provided cert):" -ForegroundColor Yellow
Write-Host " - https://$fqdn" -ForegroundColor Yellow
} else {
Write-Host "Deployment complete. Endpoint:" -ForegroundColor Green
Write-Host " - https://$fqdn" -ForegroundColor Green
if ($TlsCertPath -and $TlsKeyPath) {
Write-Host "TLS mode: using provided certificate and key." -ForegroundColor Green
} else {
Write-Host "TLS mode: self-signed fallback (demo only)." -ForegroundColor Yellow
}
}
}

if ($Cleanup) {
$config = Get-Config
if ($config -and $config.resourceGroup) {
Invoke-Az "Start cleanup of resource group" { az group delete --name $config.resourceGroup --yes --no-wait }
Write-Host "Cleanup started for resource group $($config.resourceGroup)" -ForegroundColor Green
} else {
Write-Host "No acr-config.json found with resource group info." -ForegroundColor Yellow
}
}

if (-not ($Build -or $Deploy -or $Cleanup)) {
Write-Host "Usage: .\Deploy-AutomotiveMachineVision.ps1 -Build [-ImageTag amv-20260602] | -Deploy [-ImageTag amv-20260602 -CpuCores 30 -MemoryInGB 120 -ProcessingWorkers 24 -DetectEveryNFrames 1 -TlsCertPath .\certs\fullchain.pem -TlsKeyPath .\certs\privkey.pem] [-DeployAFD] | -Cleanup"
}
29 changes: 29 additions & 0 deletions automotive-machine-vision/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
FROM mcr.microsoft.com/aci/skr:2.13 AS skr-source

FROM python:3.13-slim-bookworm

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
supervisor nginx openssl ffmpeg \
&& rm -rf /var/lib/apt/lists/*

COPY --from=skr-source /bin/skr /usr/local/bin/skr
RUN chmod +x /usr/local/bin/skr

COPY requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .
COPY templates/ templates/
COPY start.sh /app/start.sh
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY nginx.conf /etc/nginx/nginx.conf

RUN mkdir -p /var/log/supervisor /tmp/uploads /tmp/processed \
&& chmod +x /app/start.sh

ENV FLASK_SECRET_KEY=change-me-in-production
EXPOSE 443

CMD ["/app/start.sh"]
82 changes: 82 additions & 0 deletions automotive-machine-vision/Get-LetsEncryptCertificate.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
param(
[Parameter(Mandatory = $true)]
[string]$Domain,

[Parameter(Mandatory = $true)]
[string]$Email,

[ValidateSet("dns-manual", "http-standalone")]
[string]$Challenge = "dns-manual",

[switch]$UseStaging
)

$ErrorActionPreference = "Stop"

function Ensure-Command {
param([string]$Name)
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
throw "Required command '$Name' was not found in PATH."
}
}

Ensure-Command -Name "docker"

$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$certRoot = Join-Path $scriptDir "certs"
$liveRoot = Join-Path $certRoot "live"
$workRoot = Join-Path $certRoot "work"
$logsRoot = Join-Path $certRoot "logs"

New-Item -ItemType Directory -Force -Path $liveRoot, $workRoot, $logsRoot | Out-Null

$challengeArgs = @()
switch ($Challenge) {
"dns-manual" {
$challengeArgs = @("--manual", "--preferred-challenges", "dns", "--manual-public-ip-logging-ok")
}
"http-standalone" {
$challengeArgs = @("--standalone", "--preferred-challenges", "http")
Write-Host "HTTP challenge requires inbound port 80 to this machine during validation." -ForegroundColor Yellow
}
}

$serverArgs = @()
if ($UseStaging) {
$serverArgs = @("--staging")
Write-Host "Using Let's Encrypt staging endpoint." -ForegroundColor Yellow
}

$dockerArgs = @(
"run", "--rm", "-it",
"-p", "80:80",
"-v", "$($liveRoot):/etc/letsencrypt",
"-v", "$($workRoot):/var/lib/letsencrypt",
"-v", "$($logsRoot):/var/log/letsencrypt",
"certbot/certbot",
"certonly",
"--agree-tos",
"--no-eff-email",
"-m", $Email,
"-d", $Domain
) + $challengeArgs + $serverArgs

Write-Host "Requesting Let's Encrypt certificate for $Domain ..." -ForegroundColor Cyan
& docker @dockerArgs
if ($LASTEXITCODE -ne 0) {
throw "Certbot failed with exit code $LASTEXITCODE."
}

$fullchainPath = Join-Path $liveRoot "$Domain/fullchain.pem"
$privkeyPath = Join-Path $liveRoot "$Domain/privkey.pem"

if (-not (Test-Path $fullchainPath) -or -not (Test-Path $privkeyPath)) {
throw "Certificate request completed but expected files were not found."
}

Write-Host "Certificate artifacts created:" -ForegroundColor Green
Write-Host " - Certificate: $fullchainPath" -ForegroundColor Green
Write-Host " - Private key: $privkeyPath" -ForegroundColor Green
Write-Host ""
Write-Host "Deploy with:" -ForegroundColor Cyan
Write-Host ".\Deploy-AutomotiveMachineVision.ps1 -Deploy -TlsCertPath \"$fullchainPath\" -TlsKeyPath \"$privkeyPath\""
Loading
Loading