Skip to content
Open
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
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,31 @@ This step creates the following defaults:
2. component files in the components folder called `pubsub.yaml` and `statestore.yaml`.
3. default config file `$HOME/.dapr/config.yaml` for Linux/MacOS or for Windows at `%USERPROFILE%\.dapr\config.yaml` to enable tracing on `dapr init` call. Can be overridden with the `--config` flag on `dapr run`.

#### Initialize Dapr with mTLS (self-hosted)

To enable mutual TLS and start the local Sentry certificate authority in one step:

```bash
dapr init --enable-mtls
```

This command:

- Generates root and issuer certificates under `$HOME/.dapr/certs/` (`ca.crt`, `issuer.crt`, `issuer.key`)
- Starts the `dapr_sentry` container alongside placement, scheduler, redis, and zipkin
- Writes `spec.mtls.enabled: true` to the default `$HOME/.dapr/config.yaml`

After that, `dapr run` uses the default config and obtains a SPIFFE workload identity from Sentry without setting `DAPR_TRUST_ANCHORS`, `DAPR_CERT_CHAIN`, or `DAPR_CERT_KEY` manually.

Output should look like:

```
ℹ️ dapr_sentry container is running.
ℹ️ Sentry running, mTLS enabled, trust domain: cluster.local
```

`dapr uninstall` removes the Sentry container along with the other self-hosted services.

#### Slim Init

Alternatively to the above, to have the CLI not install any default configuration files or run Docker containers, use the `--slim` flag with the init command. Only Dapr binaries will be installed.
Expand Down
4 changes: 4 additions & 0 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ dapr init --runtime-path <path-to-install-directory>
# Initialize Dapr in self-hosted mode with Redis Stack for RediSearch/vector store support
dapr init --redis-stack

# Initialize Dapr in self-hosted mode with mTLS enabled (starts Sentry service)
dapr init --enable-mtls

# See more at: https://docs.dapr.io/getting-started/
`,
Run: func(cmd *cobra.Command, args []string) {
Expand Down Expand Up @@ -188,6 +191,7 @@ dapr init --redis-stack
RuntimeVersion: runtimeVersion,
DockerNetwork: dockerNetwork,
SlimMode: slimMode,
EnableMTLS: cmd.Flags().Changed("enable-mtls") && enableMTLS,
ImageRegistryURL: imageRegistryURI,
FromDir: fromDir,
ContainerRuntime: containerRuntime,
Expand Down
5 changes: 5 additions & 0 deletions pkg/standalone/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (

defaultDaprBinDirName = "bin"
defaultComponentsDirName = "components"
defaultCertsDirName = "certs"
defaultSchedulerDirName = "scheduler"
defaultSchedulerDataDirName = "data"
)
Expand Down Expand Up @@ -84,3 +85,7 @@ func GetDaprComponentsPath(daprDir string) string {
func GetDaprConfigPath(daprDir string) string {
return path_filepath.Join(daprDir, DefaultConfigFileName)
}

func GetDaprCertsPath(daprDir string) string {
return path_filepath.Join(daprDir, defaultCertsDirName)
}
151 changes: 151 additions & 0 deletions pkg/standalone/mtls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
Copyright 2026 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package standalone

import (
"fmt"
"os"
"runtime"
"sync"

"github.com/dapr/cli/utils"
"gopkg.in/yaml.v2"
)

const (
credentialsContainerPath = "/var/run/dapr/credentials"
trustAnchorsContainerPath = credentialsContainerPath + "/" + trustAnchorsFile
sentryAddressHostGateway = "host.docker.internal:50001"
)

func sentryAddressForContainer(dockerNetwork string) string {
if dockerNetwork != "" {
return DaprSentryContainerName + ":50001"
}
return sentryAddressHostGateway
}

func appendDockerHostGateway(dockerRunArgs []string, dockerNetwork string) []string {
if dockerNetwork == "" && runtime.GOOS != daprWindowsOS {
return append(dockerRunArgs, "--add-host=host.docker.internal:host-gateway")
}
return dockerRunArgs
}

func appendMTLSCredentialsMount(dockerRunArgs []string, installDir string) []string {
certsDir := GetDaprCertsPath(installDir)
return append(dockerRunArgs, "-v", certsDir+":"+credentialsContainerPath)
}

func mtlsControlPlaneServiceArgs(dockerNetwork string) []string {
return []string{
"--mode", sentryStandaloneMode,
"--tls-enabled",
"--trust-domain", defaultTrustDomain,
"--trust-anchors-file", trustAnchorsContainerPath,
"--sentry-address", sentryAddressForContainer(dockerNetwork),
}
}

func appendMTLSContainerRunArgs(dockerRunArgs []string, info initInfo) []string {
if !info.enableMTLS {
return dockerRunArgs
}
dockerRunArgs = appendMTLSCredentialsMount(dockerRunArgs, info.installDir)
return appendDockerHostGateway(dockerRunArgs, info.dockerNetwork)
}

func buildSentryContainerRunArgs(info initInfo, image string) []string {
certsDir := GetDaprCertsPath(info.installDir)
configPath := GetDaprConfigPath(info.installDir)
sentryContainerName := utils.CreateContainerName(DaprSentryContainerName, info.dockerNetwork)

args := []string{
"run",
"--name", sentryContainerName,
"--restart", "always",
"-d",
"--entrypoint", "./sentry",
"-v", certsDir + ":/var/run/dapr/credentials",
"-v", configPath + ":" + sentryConfigContainerPath + ":ro",
}

if info.dockerNetwork != "" {
args = append(args,
"--network", info.dockerNetwork,
"--network-alias", DaprSentryContainerName)
} else {
args = append(args,
"-p", fmt.Sprintf("%v:50001", sentryGRPCPort),
"-p", fmt.Sprintf("%v:8080", sentryHealthPort),
"-p", fmt.Sprintf("%v:9090", sentryMetricPort),
)
}

args = append(args, image,
"--mode", sentryStandaloneMode,
"--config", sentryConfigContainerPath,
"--issuer-credentials", credentialsContainerPath,
"--trust-domain", defaultTrustDomain,
)

return args
}

func mergeMTLSIntoConfiguration(filePath string) error {
b, err := os.ReadFile(filePath)
if err != nil {
return err
}

var config configuration
if err := yaml.Unmarshal(b, &config); err != nil {
return err
}
if config.APIVersion == "" {
config.APIVersion = "dapr.io/v1alpha1"
}
if config.Kind == "" {
config.Kind = "Configuration"
}
if config.Metadata.Name == "" {
config.Metadata.Name = "daprConfig"
}
config.Spec.MTLS.Enabled = true

out, err := yaml.Marshal(&config)
if err != nil {
return err
}
return os.WriteFile(filePath, out, 0o644)
}

func runParallelInitSteps(steps []func(*sync.WaitGroup, chan<- error, initInfo), info initInfo) error {
var wg sync.WaitGroup
errorChan := make(chan error, len(steps))
wg.Add(len(steps))
for _, step := range steps {
go step(&wg, errorChan, info)
}
go func() {
wg.Wait()
close(errorChan)
}()
for err := range errorChan {
if err != nil {
return err
}
}
return nil
}
119 changes: 119 additions & 0 deletions pkg/standalone/mtls_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
Copyright 2026 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package standalone

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestSentryAddressForContainer(t *testing.T) {
assert.Equal(t, "dapr_sentry:50001", sentryAddressForContainer("dapr-network"))
assert.Equal(t, sentryAddressHostGateway, sentryAddressForContainer(""))
}

func TestMTLSControlPlaneServiceArgs(t *testing.T) {
args := mtlsControlPlaneServiceArgs("")
assert.Contains(t, args, "--tls-enabled")
assert.Contains(t, args, "--trust-domain")
assert.Contains(t, args, defaultTrustDomain)
assert.Contains(t, args, "--trust-anchors-file")
assert.Contains(t, args, trustAnchorsContainerPath)
assert.Contains(t, args, "--sentry-address")
assert.Contains(t, args, sentryAddressHostGateway)
}

func TestBuildSentryContainerRunArgs(t *testing.T) {
installDir := t.TempDir()
certsDir := GetDaprCertsPath(installDir)
require.NoError(t, os.MkdirAll(certsDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(certsDir, trustAnchorsFile), []byte("test"), 0o600))
require.NoError(t, os.WriteFile(GetDaprConfigPath(installDir), []byte("apiVersion: dapr.io/v1alpha1"), 0o644))

info := initInfo{
installDir: installDir,
enableMTLS: true,
}
args := buildSentryContainerRunArgs(info, "daprio/dapr:1.18.1")

assert.Contains(t, args, "--mode")
assert.Contains(t, args, sentryStandaloneMode)
assert.Contains(t, args, "--config")
assert.Contains(t, args, sentryConfigContainerPath)
assert.Contains(t, args, "--issuer-credentials")
assert.Contains(t, args, credentialsContainerPath)
assert.Contains(t, args, "--trust-domain")
assert.Contains(t, args, defaultTrustDomain)
assert.Contains(t, args, "dapr_sentry")
}

func TestMergeMTLSIntoConfiguration(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "config.yaml")
existing := `apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: daprConfig
spec:
tracing:
samplingRate: "1"
zipkin:
endpointAddress: http://localhost:9411/api/v2/spans
`
require.NoError(t, os.WriteFile(configPath, []byte(existing), 0o644))

require.NoError(t, mergeMTLSIntoConfiguration(configPath))

content, err := os.ReadFile(configPath)
require.NoError(t, err)
text := string(content)
assert.Contains(t, text, "mtls:")
assert.Contains(t, text, "enabled: true")
assert.Contains(t, text, "zipkin:")
}

func TestGenerateCertsForMTLSInternal(t *testing.T) {
installDir := t.TempDir()
info := initInfo{
installDir: installDir,
enableMTLS: true,
}

require.NoError(t, generateCertsForMTLSInternal(info))

certsDir := GetDaprCertsPath(installDir)
for _, file := range []string{trustAnchorsFile, issuerCertFile, issuerKeyFile} {
path := filepath.Join(certsDir, file)
assert.FileExists(t, path)
stat, err := os.Stat(path)
require.NoError(t, err)
assert.Equal(t, os.FileMode(0o600), stat.Mode().Perm())
}

require.NoError(t, generateCertsForMTLSInternal(info))
}

func TestContainersToRemoveIncludesSentry(t *testing.T) {
containers := containersToRemove(true, false, false)
for _, c := range containers {
if c.name == DaprSentryContainerName {
assert.False(t, c.warnIfMissing)
return
}
}
t.Fatal("expected sentry container in removal list")
}
29 changes: 28 additions & 1 deletion pkg/standalone/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"net"
"os"
"os/exec"
"path/filepath"
"reflect"
"runtime"
"strconv"
Expand All @@ -32,6 +33,7 @@ import (

"github.com/dapr/cli/pkg/print"
localloader "github.com/dapr/dapr/pkg/components/loader"
"github.com/dapr/dapr/pkg/security/consts"
)

type LogDestType string
Expand Down Expand Up @@ -384,7 +386,11 @@ func (config *RunConfig) getArgs() []string {
sentryAddress := mtlsEndpoint(config.ConfigFile)
if sentryAddress != "" {
// mTLS is enabled locally, set it up.
args = append(args, "--enable-mtls", "--sentry-address", sentryAddress)
args = append(args,
"--enable-mtls",
"--sentry-address", sentryAddress,
"--control-plane-trust-domain", defaultTrustDomain,
)
}
}

Expand Down Expand Up @@ -581,9 +587,30 @@ func GetDaprCommand(config *RunConfig) (*exec.Cmd, error) {

args := config.getArgs()
cmd := exec.Command(daprCMD, args...)
if trustAnchors, ok := mtlsTrustAnchors(config); ok {
cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", consts.TrustAnchorsEnvVar, trustAnchors))
}
return cmd, nil
}

func mtlsTrustAnchors(config *RunConfig) (string, bool) {
if config == nil || mtlsEndpoint(config.ConfigFile) == "" {
return "", false
}

daprDir, err := GetDaprRuntimePath(config.DaprdInstallPath)
if err != nil {
return "", false
}

trustAnchors, err := os.ReadFile(filepath.Join(GetDaprCertsPath(daprDir), trustAnchorsFile))
if err != nil {
return "", false
}

return string(trustAnchors), true
}

func mtlsEndpoint(configFile string) string {
if configFile == "" {
return ""
Expand Down
Loading