Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

appsec: Service Extension callout #2965

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4327d10
Service Extension Callout (Envoy external processing)
e-n-0 Aug 30, 2024
a2f01c2
Remove binary to move it to another branch
e-n-0 Nov 5, 2024
f3bcc4d
Fix CI
e-n-0 Nov 5, 2024
e3a3dbc
Apply comments
e-n-0 Nov 8, 2024
75b6240
revert all changes expect in contrib/envoyproxy
eliottness Nov 8, 2024
e210f05
rework to fake a new http.Request and a http.ResponseWriter
eliottness Nov 8, 2024
89ebfe6
Apply comments
e-n-0 Nov 20, 2024
24bb334
Add "t.Helper" to helper methods
e-n-0 Nov 20, 2024
46a08b1
Downgrade grpc version to v1.64.0 (some proto changes)
e-n-0 Nov 28, 2024
c789009
Add example_test
e-n-0 Nov 28, 2024
961d73d
update some comments
e-n-0 Nov 28, 2024
ee0cd57
go mod tidy (rebase fix)
e-n-0 Dec 11, 2024
22d7095
Applied comments
e-n-0 Dec 11, 2024
60ce0f8
Merge branch 'main' into flavien/service-extensions
eliottness Dec 12, 2024
c86812b
apply comments
e-n-0 Dec 12, 2024
96d0780
Service Extension Callout (Envoy external processing)
e-n-0 Aug 30, 2024
f8234d5
revert all changes expect in contrib/envoyproxy
eliottness Nov 8, 2024
5bbceba
rework to fake a new http.Request and a http.ResponseWriter
eliottness Nov 8, 2024
9a0c30b
Downgrade grpc version to v1.64.0 (some proto changes)
e-n-0 Nov 28, 2024
74b6439
Service Extension Callout (Envoy external processing)
e-n-0 Aug 30, 2024
56db2bd
Update the action
e-n-0 Nov 6, 2024
fb89247
Update Telemetry test: exclude cmd
e-n-0 Nov 6, 2024
4b8293e
Rebase + Apply comments
e-n-0 Nov 28, 2024
4d54b63
fix rebase
e-n-0 Nov 28, 2024
a817ec3
fix rebase
e-n-0 Nov 28, 2024
eb7ffc8
Generate self signed certificates instead of importing them
e-n-0 Dec 11, 2024
8d369a9
Add Readme
e-n-0 Dec 11, 2024
fbfcda1
fix rebase
e-n-0 Dec 11, 2024
f2cbd41
update to register the service
e-n-0 Dec 12, 2024
673ec27
Multi build runners (arm64, amd64)
e-n-0 Dec 16, 2024
c66d4fd
Merge branch 'main' into flavien/service-extensions-part2
e-n-0 Dec 16, 2024
a8c930b
Merge branch 'main' into flavien/service-extensions-part2
e-n-0 Dec 18, 2024
6e34f2e
Set resource name
e-n-0 Dec 16, 2024
22f3e0e
Applied comments (ipenv, signals, gracefull shutdown)
e-n-0 Dec 18, 2024
e3cc085
Applied comments
e-n-0 Dec 19, 2024
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
130 changes: 130 additions & 0 deletions .github/workflows/service-extensions-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
name: Publish Service Extensions Callout images packages

on:
push:
tags:
- 'v*.*'
workflow_dispatch:
inputs:
tag_name:
description: 'Docker image tag to use for the package'
required: true
default: 'dev'
commit_sha:
description: 'Commit SHA to checkout'
required: true
set_as_latest:
description: 'Set the tag as latest'
required: false
default: 'false'

permissions:
contents: read
packages: write

env:
TAG_NAME: ${{ github.ref_name || github.event.inputs.tag_name }}
REF_NAME: ${{ github.ref || github.event.inputs.commit_sha }}
COMMIT_SHA: ${{ github.sha || github.event.inputs.commit_sha }}
PUSH_LATEST: ${{ github.event.inputs.set_as_latest || 'true' }}
REGISTRY_IMAGE: ghcr.io/datadog/dd-trace-go/service-extensions-callout

jobs:
build-service-extensions:
runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-latest' || 'arm-4core-linux' }}
strategy:
matrix:
platform: [ linux/amd64, linux/arm64 ]

steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV

- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ env.REF_NAME }}

- name: Install Docker (only arm64)
if: matrix.platform == 'linux/arm64'
run: |
sudo apt-get update
sudo apt-get install -y docker.io
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker $USER
newgrp docker
sudo chmod 666 /var/run/docker.sock

- name: Set up Docker Buildx
uses: docker/[email protected]

- name: Login to Docker
shell: bash
run: docker login -u publisher -p ${{ secrets.GITHUB_TOKEN }} ghcr.io

- name: Docker meta
id: meta
uses: docker/metadata-action@v5
e-n-0 marked this conversation as resolved.
Show resolved Hide resolved
with:
images: ${{ env.REGISTRY_IMAGE }}

- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
e-n-0 marked this conversation as resolved.
Show resolved Hide resolved
e-n-0 marked this conversation as resolved.
Show resolved Hide resolved
with:
context: .
file: ./contrib/envoyproxy/go-control-plane/cmd/serviceextensions/Dockerfile
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true

- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"

- name: Upload digest
uses: actions/upload-artifact@v4
e-n-0 marked this conversation as resolved.
Show resolved Hide resolved
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1

publish-service-extensions:
runs-on: ubuntu-latest
needs:
- build-service-extensions

steps:
- name: Download digests
uses: actions/download-artifact@v4
e-n-0 marked this conversation as resolved.
Show resolved Hide resolved
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true

- name: Set up Docker Buildx
uses: docker/[email protected]
e-n-0 marked this conversation as resolved.
Show resolved Hide resolved

- name: Login to Docker
shell: bash
run: docker login -u publisher -p ${{ secrets.GITHUB_TOKEN }} ghcr.io

- name: Create tags
id: tags
run: |
tagname=${TAG_NAME//\//-} # remove slashes from tag name
echo "tags=-t ghcr.io/datadog/dd-trace-go/service-extensions-callout:${tagname} \
-t ghcr.io/datadog/dd-trace-go/service-extensions-callout:${{ env.COMMIT_SHA }} \
${{ env.PUSH_LATEST == 'true' && '-t ghcr.io/datadog/dd-trace-go/service-extensions-callout:latest' }}" >> $GITHUB_OUTPUT

- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create ${{ steps.tags.outputs.tags }} \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
serviceextensions
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Build stage
FROM golang:1.23-alpine AS builder
ENV CGO_ENABLED=1

WORKDIR /app
COPY . .

RUN apk add --no-cache --update git build-base openssl

# Generate SSL self-signed localhost certificate
RUN openssl genrsa -out localhost.key 3072
RUN openssl req -new \
-key localhost.key \
-subj "/C=US/ST=New York/O=Datadog/OU=gRPC/CN=localhost" \
-out request.csr
RUN openssl x509 -req -days 3660 \
-in request.csr \
-signkey localhost.key \
-out localhost.crt

# Build the serviceextensions binary
RUN go build -tags=appsec -o ./contrib/envoyproxy/go-control-plane/cmd/serviceextensions/serviceextensions ./contrib/envoyproxy/go-control-plane/cmd/serviceextensions

# Runtime stage
FROM alpine:3.20.3
RUN apk --no-cache add ca-certificates tzdata libc6-compat libgcc libstdc++
WORKDIR /app
COPY --from=builder /app/contrib/envoyproxy/go-control-plane/cmd/serviceextensions/serviceextensions /app/serviceextensions
COPY --from=builder /app/localhost.crt /app/localhost.crt
COPY --from=builder /app/localhost.key /app/localhost.key

EXPOSE 80
EXPOSE 443

e-n-0 marked this conversation as resolved.
Show resolved Hide resolved
CMD ["./serviceextensions"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# ASM Service Extension

[GCP Services Extensions](https://cloud.google.com/service-extensions/docs/overview) enable Google Cloud users to provide programmability and extensibility on Cloud Load Balancing data paths and at the edge.

## Installation

### From Release

This package provides a docker image to be used with Google Cloud Service Extensions.
The images are published at each release of the tracer and can be found in [the repo registry](https://github.com/DataDog/dd-trace-go/pkgs/container/dd-trace-go%2Fservice-extensions-callout).

### Build image

The docker image can be build locally using docker. Start by cloning the `dd-trace-go` repo, `cd` inside it and run that command:
```sh
docker build --build-arg -f contrib/envoyproxy/go-control-plane/cmd/serviceextensions/Dockerfile -t datadog/dd-trace-go/service-extensions-callout:local .
```

## Configuration

The ASM Service Extension expose some configuration. The configuration can be tweaked if the Service Extension is only used as an External Processor for Envoy that is not operated by GCP.

>**GCP requires that the default configuration for the Service Extension should not change.**

| Environment variable | Default value | Description |
|---|---|---|
| `DD_SERVICE_EXTENSION_HOST` | `0.0.0.0` | Host on where the gRPC and HTTP server should listen to. |
| `DD_SERVICE_EXTENSION_PORT` | `443` | Port used by the gRPC Server.<br>Envoy Google backend’s is only using secure connection to Service Extension. |
| `DD_SERVICE_EXTENSION_HEALTHCHECK_PORT` | `80` | Port used for the HTTP server for the health check. |

> The Service Extension need to be connected to a deployed [Datadog agent](https://docs.datadoghq.com/agent).

| Environment variable | Default value | Description |
|---|---|---|
| `DD_AGENT_HOST` | `N/A` | Host of a running Datadog Agent. |
| `DD_TRACE_AGENT_PORT` | `8126` | Port of a running Datadog Agent. |

### SSL Configuration

The Envoy of GCP is configured to communicate to the Service Extension with TLS.

`localhost` self signed certificates are generated and bundled into the ASM Service Extension docker image and loaded at the start of the gRPC server.
154 changes: 154 additions & 0 deletions contrib/envoyproxy/go-control-plane/cmd/serviceextensions/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016 Datadog, Inc.

package main

import (
"crypto/tls"
"gopkg.in/DataDog/dd-trace-go.v1/internal"
"net"
"net/http"
"os"
"strconv"

gocontrolplane "gopkg.in/DataDog/dd-trace-go.v1/contrib/envoyproxy/go-control-plane"
"gopkg.in/DataDog/dd-trace-go.v1/internal/log"
"gopkg.in/DataDog/dd-trace-go.v1/internal/version"

extproc "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
"github.com/gorilla/mux"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/reflection"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
)

// AppsecCalloutExtensionService defines the struct that follows the ExternalProcessorServer interface.
type AppsecCalloutExtensionService struct {
extproc.ExternalProcessorServer
}

type serviceExtensionConfig struct {
extensionPort string
extensionHost string
healthcheckPort string
}

func loadConfig() serviceExtensionConfig {
extensionPortInt := internal.IntEnv("DD_SERVICE_EXTENSION_PORT", 443)
if extensionPortInt < 1 || extensionPortInt > 65535 {
log.Error("service_extension: invalid port number: %d\n", extensionPortInt)
os.Exit(1)
}
e-n-0 marked this conversation as resolved.
Show resolved Hide resolved

healthcheckPortInt := internal.IntEnv("DD_SERVICE_EXTENSION_HEALTHCHECK_PORT", 80)
if healthcheckPortInt < 1 || healthcheckPortInt > 65535 {
log.Error("service_extension: invalid port number: %d\n", healthcheckPortInt)
os.Exit(1)
}

extensionHost := internal.IpEnv("DD_SERVICE_EXTENSION_HOST", "0.0.0.0")
extensionPortStr := strconv.FormatInt(int64(extensionPortInt), 10)
healthcheckPortStr := strconv.FormatInt(int64(healthcheckPortInt), 10)

// check if the ports are free
l, err := net.Listen("tcp", extensionHost+":"+extensionPortStr)
if err != nil {
log.Error("service_extension: failed to listen on extension %s:%s: %v\n", extensionHost, extensionPortStr, err)
os.Exit(1)
}
err = l.Close()
if err != nil {
log.Error("service_extension: failed to close listener on %s:%s: %v\n", extensionHost, extensionPortStr, err)
os.Exit(1)
}

l, err = net.Listen("tcp", extensionHost+":"+healthcheckPortStr)
if err != nil {
log.Error("service_extension: failed to listen on health check %s:%s: %v\n", extensionHost, healthcheckPortStr, err)
os.Exit(1)
}
err = l.Close()
if err != nil {
log.Error("service_extension: failed to close listener on %s:%s: %v\n", extensionHost, healthcheckPortStr, err)
os.Exit(1)
}

return serviceExtensionConfig{
extensionPort: extensionPortStr,
extensionHost: extensionHost,
healthcheckPort: healthcheckPortStr,
}
}

func main() {
var extensionService AppsecCalloutExtensionService

// Set the DD_VERSION to the current tracer version if not set
if os.Getenv("DD_VERSION") == "" {
if err := os.Setenv("DD_VERSION", version.Tag); err != nil {
log.Error("service_extension: failed to set DD_VERSION environment variable: %v\n", err)
}
}

config := loadConfig()

tracer.Start(tracer.WithAppSecEnabled(true))
// TODO: Enable ASM standalone mode when it is developed (should be done for Q4 2024)

go StartGPRCSsl(&extensionService, config)
log.Info("service_extension: callout gRPC server started on %s:%s\n", config.extensionHost, config.extensionPort)

go startHealthCheck(config)
log.Info("service_extension: health check server started on %s:%s\n", config.extensionHost, config.healthcheckPort)

select {}
e-n-0 marked this conversation as resolved.
Show resolved Hide resolved
}

func startHealthCheck(config serviceExtensionConfig) {
muxServer := mux.NewRouter()
muxServer.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status": "ok", "library": {"language": "golang", "version": "` + version.Tag + `"}}`))
})

server := &http.Server{
Addr: config.extensionHost + ":" + config.healthcheckPort,
Handler: muxServer,
}

if err := server.ListenAndServe(); err != nil {
e-n-0 marked this conversation as resolved.
Show resolved Hide resolved
log.Error("service_extension: error starting health check http server: %v\n", err)
}
}

func StartGPRCSsl(service extproc.ExternalProcessorServer, config serviceExtensionConfig) {
cert, err := tls.LoadX509KeyPair("localhost.crt", "localhost.key")
if err != nil {
log.Error("service_extension: failed to load key pair: %v\n", err)
os.Exit(1)
return
}

lis, err := net.Listen("tcp", config.extensionHost+":"+config.extensionPort)
if err != nil {
log.Error("service_extension: gRPC server failed to listen: %v\n", err)
os.Exit(1)
return
}

grpcCredentials := credentials.NewServerTLSFromCert(&cert)
grpcServer := grpc.NewServer(grpc.Creds(grpcCredentials))

appsecEnvoyExternalProcessorServer := gocontrolplane.AppsecEnvoyExternalProcessorServer(service)

extproc.RegisterExternalProcessorServer(grpcServer, appsecEnvoyExternalProcessorServer)
reflection.Register(grpcServer)
e-n-0 marked this conversation as resolved.
Show resolved Hide resolved
if err := grpcServer.Serve(lis); err != nil {
log.Error("service_extension: error starting gRPC server: %v\n", err)
os.Exit(1)
}
}
2 changes: 1 addition & 1 deletion contrib/internal/telemetrytest/telemetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func TestTelemetryEnabled(t *testing.T) {
packages = append(packages, out)
}
for _, pkg := range packages {
if strings.Contains(pkg.ImportPath, "/test") || strings.Contains(pkg.ImportPath, "/internal") {
if strings.Contains(pkg.ImportPath, "/test") || strings.Contains(pkg.ImportPath, "/internal") || strings.Contains(pkg.ImportPath, "/cmd") {
continue
}
if !pkg.hasTelemetryImport(t) {
Expand Down
2 changes: 1 addition & 1 deletion ddtrace/tracer/option_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ func TestIntegrationEnabled(t *testing.T) {
packages = append(packages, out)
}
for _, pkg := range packages {
if strings.Contains(pkg.ImportPath, "/test") || strings.Contains(pkg.ImportPath, "/internal") {
if strings.Contains(pkg.ImportPath, "/test") || strings.Contains(pkg.ImportPath, "/internal") || strings.Contains(pkg.ImportPath, "/cmd") {
continue
}
p := strings.Replace(pkg.Dir, pkg.Root, "../..", 1)
Expand Down
Loading
Loading