Skip to content

Commit 00d59e6

Browse files
codefromthecryptekline-ai
authored andcommitted
feat(host): auto-generate certificates for host mode (envoyproxy#7362)
* feat(host): auto-generate certificates for host mode Auto-generates TLS certificates for host mode when they don't exist, eliminating the manual 'envoy-gateway certgen --local' step. Also fixes a data race in proxy context map by using atomic LoadAndDelete operation. Signed-off-by: Adrian Cole <[email protected]> Signed-off-by: EkLine AI <[email protected]>
1 parent e6a2406 commit 00d59e6

File tree

9 files changed

+531
-91
lines changed

9 files changed

+531
-91
lines changed

examples/extension-server/go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ go 1.25.3
44

55
require (
66
github.com/envoyproxy/gateway v1.3.1
7-
github.com/envoyproxy/go-control-plane v0.13.5-0.20250929230642-07d3df27ff4f
8-
github.com/envoyproxy/go-control-plane/envoy v1.35.1-0.20250929230642-07d3df27ff4f
7+
github.com/envoyproxy/go-control-plane v0.13.5-0.20251022160057-de4316c523b7
8+
github.com/envoyproxy/go-control-plane/envoy v1.35.1-0.20251022160057-de4316c523b7
99
github.com/urfave/cli/v2 v2.27.7
1010
google.golang.org/grpc v1.76.0
1111
google.golang.org/protobuf v1.36.10

examples/extension-server/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
2424
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2525
github.com/envoyproxy/go-control-plane v0.13.5-0.20250929230642-07d3df27ff4f h1:36vvJBe/wXWfD7qrTb1WnbPVPMxNFDfEygztH8wgebw=
2626
github.com/envoyproxy/go-control-plane v0.13.5-0.20250929230642-07d3df27ff4f/go.mod h1:PTY7yDlLxB4bW7rEOO7e79uTDr9yXzpuI1QGIDfxEzc=
27+
github.com/envoyproxy/go-control-plane v0.13.5-0.20251022160057-de4316c523b7/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs=
2728
github.com/envoyproxy/go-control-plane/envoy v1.35.1-0.20250929230642-07d3df27ff4f h1:4efYrIQgVRwCmwCveby6ck+VpxqzibdOL1Out1rJqqc=
2829
github.com/envoyproxy/go-control-plane/envoy v1.35.1-0.20250929230642-07d3df27ff4f/go.mod h1:2LcmvJoXsDSrsGZIxGM0Gah9ykiwTn/kgjyQdnNH8Jc=
30+
github.com/envoyproxy/go-control-plane/envoy v1.35.1-0.20251022160057-de4316c523b7/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
2931
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
3032
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
3133
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=

internal/crypto/certgen.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ func GenerateCerts(cfg *config.Server) (*Certificates, error) {
110110
case egv1a1.ProviderTypeKubernetes:
111111
egDNSNames = kubeServiceNames(DefaultEnvoyGatewayDNSPrefix, cfg.ControllerNamespace, cfg.DNSDomain)
112112
envoyDNSNames = append(envoyDNSNames, fmt.Sprintf("*.%s", cfg.ControllerNamespace))
113+
case egv1a1.ProviderTypeCustom:
114+
// For custom provider (host mode), use localhost for xDS communication
115+
egDNSNames = []string{"localhost"}
116+
envoyDNSNames = []string{"localhost"}
113117
default:
114118
// Kubernetes is the only supported Envoy Gateway provider.
115119
return nil, fmt.Errorf("unsupported provider type %v", egProvider)

internal/crypto/certgen_test.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,24 @@ import (
1616

1717
"github.com/stretchr/testify/require"
1818

19+
egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
1920
"github.com/envoyproxy/gateway/internal/envoygateway/config"
2021
)
2122

2223
func TestGenerateCerts(t *testing.T) {
2324
type testcase struct {
24-
certConfig *Configuration
25+
cfg *config.Server
2526
wantEnvoyGatewayDNSName string
2627
wantEnvoyDNSName string
2728
}
2829

29-
cfg, err := config.New(os.Stdout, os.Stderr)
30-
require.NoError(t, err)
31-
3230
run := func(t *testing.T, name string, tc testcase) {
3331
t.Helper()
3432

3533
t.Run(name, func(t *testing.T) {
3634
t.Helper()
3735

38-
got, err := GenerateCerts(cfg)
36+
got, err := GenerateCerts(tc.cfg)
3937
require.NoError(t, err)
4038

4139
roots := x509.NewCertPool()
@@ -52,11 +50,29 @@ func TestGenerateCerts(t *testing.T) {
5250
})
5351
}
5452

55-
run(t, "no configuration - use defaults", testcase{
56-
certConfig: &Configuration{},
53+
// Test Kubernetes provider (default)
54+
kubeCfg, err := config.New(os.Stdout, os.Stderr)
55+
require.NoError(t, err)
56+
57+
run(t, "kubernetes provider - use defaults", testcase{
58+
cfg: kubeCfg,
5759
wantEnvoyGatewayDNSName: DefaultEnvoyGatewayDNSPrefix,
5860
wantEnvoyDNSName: fmt.Sprintf("*.%s", config.DefaultNamespace),
5961
})
62+
63+
// Test Custom provider
64+
customCfg, err := config.New(os.Stdout, os.Stderr)
65+
require.NoError(t, err)
66+
// Set provider type to Custom
67+
customCfg.EnvoyGateway.Provider = &egv1a1.EnvoyGatewayProvider{
68+
Type: egv1a1.ProviderTypeCustom,
69+
}
70+
71+
run(t, "custom provider - use localhost", testcase{
72+
cfg: customCfg,
73+
wantEnvoyGatewayDNSName: "localhost",
74+
wantEnvoyDNSName: "localhost",
75+
})
6076
}
6177

6278
func TestGeneratedValidKubeCerts(t *testing.T) {

internal/infrastructure/host/infra.go

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,13 @@ import (
1111
"io"
1212
"os"
1313
"path/filepath"
14+
"sync"
15+
16+
func_e "github.com/tetratelabs/func-e"
17+
func_e_api "github.com/tetratelabs/func-e/api"
1418

1519
egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
20+
"github.com/envoyproxy/gateway/internal/crypto"
1621
"github.com/envoyproxy/gateway/internal/envoygateway/config"
1722
"github.com/envoyproxy/gateway/internal/infrastructure/common"
1823
"github.com/envoyproxy/gateway/internal/logging"
@@ -42,7 +47,7 @@ type Infra struct {
4247
EnvoyGateway *egv1a1.EnvoyGateway
4348

4449
// proxyContextMap store the context of each running proxy by its name for lifecycle management.
45-
proxyContextMap map[string]*proxyContext
50+
proxyContextMap sync.Map
4651

4752
// sdsConfigPath is the path to SDS configuration files.
4853
sdsConfigPath string
@@ -54,6 +59,9 @@ type Infra struct {
5459
Stdout io.Writer
5560
// Stderr is the writer for error output (for Envoy stderr).
5661
Stderr io.Writer
62+
63+
// envoyRunner runs Envoy (can be overridden in tests).
64+
envoyRunner func_e_api.RunFunc
5765
}
5866

5967
func NewInfra(runnerCtx context.Context, cfg *config.Server, logger logging.Logger) (*Infra, error) {
@@ -75,10 +83,10 @@ func NewInfra(runnerCtx context.Context, cfg *config.Server, logger logging.Logg
7583
return nil, fmt.Errorf("failed to create data directory: %w", err)
7684
}
7785

78-
// Check local certificates dir exist
86+
// Check if certificates exist, generate them if not
7987
certPath := paths.CertDir("envoy")
80-
if _, err := os.Lstat(certPath); err != nil {
81-
return nil, fmt.Errorf("failed to stat cert dir: %w", err)
88+
if err := maybeGenerateCertificates(cfg, certPath); err != nil {
89+
return nil, err
8290
}
8391

8492
// Ensure the sds config exist
@@ -90,11 +98,11 @@ func NewInfra(runnerCtx context.Context, cfg *config.Server, logger logging.Logg
9098
Paths: paths,
9199
Logger: logger,
92100
EnvoyGateway: cfg.EnvoyGateway,
93-
proxyContextMap: make(map[string]*proxyContext),
94101
sdsConfigPath: certPath,
95102
defaultEnvoyImage: egv1a1.DefaultEnvoyProxyImage,
96103
Stdout: cfg.Stdout,
97104
Stderr: cfg.Stderr,
105+
envoyRunner: func_e.Run,
98106
}
99107
return infra, nil
100108
}
@@ -116,3 +124,52 @@ func createSdsConfig(dir string) error {
116124

117125
return nil
118126
}
127+
128+
// maybeGenerateCertificates checks if all required certificate files exist and generates them if any is missing.
129+
func maybeGenerateCertificates(cfg *config.Server, certPath string) error {
130+
certFiles := []string{"ca.crt", "tls.crt", "tls.key"}
131+
132+
// Check if any cert file is missing
133+
var missing bool
134+
for _, filename := range certFiles {
135+
filePath := filepath.Join(certPath, filename)
136+
_, err := os.Lstat(filePath)
137+
if os.IsNotExist(err) {
138+
missing = true
139+
break
140+
}
141+
if err != nil {
142+
return fmt.Errorf("failed to stat %s: %w", filename, err)
143+
}
144+
}
145+
146+
if !missing {
147+
// All files exist, nothing to do
148+
return nil
149+
}
150+
151+
// Generate certificates automatically
152+
certs, err := crypto.GenerateCerts(cfg)
153+
if err != nil {
154+
return fmt.Errorf("failed to generate certificates: %w", err)
155+
}
156+
157+
// Create the cert directory
158+
if err := os.MkdirAll(certPath, 0o750); err != nil {
159+
return fmt.Errorf("failed to create cert directory: %w", err)
160+
}
161+
162+
// Write cert files
163+
certMap := map[string][]byte{
164+
"ca.crt": certs.CACertificate,
165+
"tls.crt": certs.EnvoyCertificate,
166+
"tls.key": certs.EnvoyPrivateKey,
167+
}
168+
169+
for filename, content := range certMap {
170+
if err := file.Write(string(content), filepath.Join(certPath, filename)); err != nil {
171+
return fmt.Errorf("failed to write %s: %w", filename, err)
172+
}
173+
}
174+
return nil
175+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Copyright Envoy Gateway Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
// The full text of the Apache license is available in the LICENSE file at
4+
// the root of the repo.
5+
6+
package host
7+
8+
import (
9+
"io"
10+
"os"
11+
"path/filepath"
12+
"testing"
13+
14+
"github.com/stretchr/testify/require"
15+
16+
"github.com/envoyproxy/gateway/internal/envoygateway/config"
17+
"github.com/envoyproxy/gateway/internal/infrastructure/common"
18+
"github.com/envoyproxy/gateway/internal/utils/file"
19+
)
20+
21+
func TestMaybeGenerateCertificates(t *testing.T) {
22+
cfg, err := config.New(io.Discard, io.Discard)
23+
require.NoError(t, err)
24+
25+
certFiles := []string{"ca.crt", "tls.crt", "tls.key"}
26+
27+
t.Run("all_files_exist", func(t *testing.T) {
28+
tmpDir := t.TempDir()
29+
certPath := filepath.Join(tmpDir, "envoy")
30+
31+
// Create directory and dummy files
32+
require.NoError(t, os.MkdirAll(certPath, 0o750))
33+
for _, filename := range certFiles {
34+
fpath := filepath.Join(certPath, filename)
35+
require.NoError(t, os.WriteFile(fpath, []byte("dummy"), 0o600))
36+
}
37+
38+
err := maybeGenerateCertificates(cfg, certPath)
39+
require.NoError(t, err)
40+
41+
// Verify files still exist and unchanged size
42+
for _, filename := range certFiles {
43+
data, err := os.ReadFile(filepath.Join(certPath, filename))
44+
require.NoError(t, err)
45+
require.Len(t, data, 5) // "dummy"
46+
}
47+
})
48+
49+
t.Run("missing_files", func(t *testing.T) {
50+
tmpDir := t.TempDir()
51+
certPath := filepath.Join(tmpDir, "envoy")
52+
53+
err := maybeGenerateCertificates(cfg, certPath)
54+
require.NoError(t, err)
55+
56+
// Verify directory created
57+
info, err := os.Stat(certPath)
58+
require.NoError(t, err)
59+
require.True(t, info.IsDir())
60+
61+
// Verify all files created and non-empty
62+
for _, filename := range certFiles {
63+
data, err := os.ReadFile(filepath.Join(certPath, filename))
64+
require.NoError(t, err)
65+
require.NotEmpty(t, data, filename)
66+
}
67+
})
68+
69+
t.Run("partial_files_missing", func(t *testing.T) {
70+
tmpDir := t.TempDir()
71+
certPath := filepath.Join(tmpDir, "envoy")
72+
73+
require.NoError(t, os.MkdirAll(certPath, 0o750))
74+
75+
// Create only one file
76+
require.NoError(t, os.WriteFile(filepath.Join(certPath, "ca.crt"), []byte("dummy"), 0o600))
77+
78+
err := maybeGenerateCertificates(cfg, certPath)
79+
require.NoError(t, err)
80+
81+
// Verify all files created and non-empty
82+
for _, filename := range certFiles {
83+
data, err := os.ReadFile(filepath.Join(certPath, filename))
84+
require.NoError(t, err)
85+
require.NotEmpty(t, data, filename)
86+
}
87+
})
88+
89+
t.Run("cert_generation_fails", func(t *testing.T) {
90+
tmpDir := t.TempDir()
91+
// This tests mkdir fail by making parent unwritable
92+
unwritableDir := filepath.Join(tmpDir, "unwritable")
93+
require.NoError(t, os.Mkdir(unwritableDir, 0o555)) // Read-only
94+
95+
badCertPath := filepath.Join(unwritableDir, "envoy")
96+
err := maybeGenerateCertificates(cfg, badCertPath)
97+
require.ErrorContains(t, err, "failed to create cert directory")
98+
})
99+
}
100+
101+
func TestCreateSdsConfig(t *testing.T) {
102+
t.Run("success", func(t *testing.T) {
103+
dir := t.TempDir()
104+
// Create required cert files
105+
require.NoError(t, file.Write("test ca", filepath.Join(dir, XdsTLSCaFilename)))
106+
require.NoError(t, file.Write("test cert", filepath.Join(dir, XdsTLSCertFilename)))
107+
require.NoError(t, file.Write("test key", filepath.Join(dir, XdsTLSKeyFilename)))
108+
109+
err := createSdsConfig(dir)
110+
require.NoError(t, err)
111+
112+
// Verify CA config was created
113+
caConfigPath := filepath.Join(dir, common.SdsCAFilename)
114+
actualCAConfig, err := os.ReadFile(caConfigPath)
115+
require.NoError(t, err)
116+
require.NotEmpty(t, actualCAConfig)
117+
118+
// Verify cert config was created
119+
certConfigPath := filepath.Join(dir, common.SdsCertFilename)
120+
actualCertConfig, err := os.ReadFile(certConfigPath)
121+
require.NoError(t, err)
122+
require.NotEmpty(t, actualCertConfig)
123+
})
124+
125+
t.Run("error_writing_ca_config", func(t *testing.T) {
126+
// Use invalid path to force file.Write to fail
127+
invalidDir := filepath.Join("/", "nonexistent", "invalid", "path")
128+
err := createSdsConfig(invalidDir)
129+
require.Error(t, err)
130+
})
131+
}

0 commit comments

Comments
 (0)