Files
zitadel/cmd/setup/03.go
T
Livio SpringandGitHub 71fab2e574 feat: allow adding trusted domains in instance setup (#11169)
# Which Problems Are Solved

When running Zitadel behind a reverse proxy and especially when the API
and the login UI don't run on the same domain, Zitadel needs to be
configured to trust the corresponding domains and use them in public
responses, like email links and more.
This can be done by adding a trusted domain. However it's currently only
possible through the API and not in the instance setup process.

# How the Problems Are Solved

Added a possibility to configure multiple trusted domains in the first
instance setup process.

# Additional Changes

None

# Additional Context

- closes #11153
2026-02-23 06:06:33 +00:00

212 lines
5.9 KiB
Go

package setup
import (
"context"
"fmt"
"os"
"strings"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/ui/login"
"github.com/zitadel/zitadel/internal/cache/connector"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/crypto"
crypto_db "github.com/zitadel/zitadel/internal/crypto/database"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
)
type FirstInstance struct {
InstanceName string
DefaultLanguage language.Tag
Org command.InstanceOrgSetup
MachineKeyPath string
PatPath string
LoginClientPatPath string
Features *command.InstanceFeatures
TrustedDomains []string
Skip bool
instanceSetup command.InstanceSetup
userEncryptionKey *crypto.KeyConfig
smtpEncryptionKey *crypto.KeyConfig
oidcEncryptionKey *crypto.KeyConfig
masterKey string
db *database.DB
es *eventstore.Eventstore
defaults systemdefaults.SystemDefaults
zitadelRoles []authz.RoleMapping
externalDomain string
externalSecure bool
externalPort uint16
domain string
defaultPaths *login.DefaultPaths
}
func (mig *FirstInstance) Execute(ctx context.Context, _ eventstore.Event) error {
if mig.Skip {
return nil
}
keyStorage, err := mig.verifyEncryptionKeys(ctx)
if err != nil {
return err
}
userAlg, err := crypto.NewAESCrypto(mig.userEncryptionKey, keyStorage)
if err != nil {
return err
}
smtpEncryption, err := crypto.NewAESCrypto(mig.smtpEncryptionKey, keyStorage)
if err != nil {
return err
}
oidcEncryption, err := crypto.NewAESCrypto(mig.oidcEncryptionKey, keyStorage)
if err != nil {
return err
}
cmd, err := command.StartCommands(ctx,
mig.es,
connector.Connectors{},
mig.defaults,
mig.zitadelRoles,
nil,
nil,
mig.externalDomain,
mig.externalSecure,
mig.externalPort,
nil,
nil,
smtpEncryption,
nil,
userAlg,
nil,
oidcEncryption,
nil,
nil,
nil,
nil,
nil,
0,
0,
0,
nil,
mig.defaultPaths,
)
if err != nil {
return err
}
mig.instanceSetup.InstanceName = mig.InstanceName
mig.instanceSetup.CustomDomain = mig.externalDomain
mig.instanceSetup.TrustedDomains = mig.TrustedDomains
mig.instanceSetup.DefaultLanguage = mig.DefaultLanguage
mig.instanceSetup.Org = mig.Org
// check if username is email style or else append @<orgname>.<custom-domain>
//this way we have the same value as before changing `UserLoginMustBeDomain` to false
if !mig.instanceSetup.DomainPolicy.UserLoginMustBeDomain && !strings.Contains(mig.instanceSetup.Org.Human.Username, "@") {
orgDomain, err := domain.NewIAMDomainName(mig.instanceSetup.Org.Name, mig.instanceSetup.CustomDomain)
if err != nil {
return err
}
mig.instanceSetup.Org.Human.Username = mig.instanceSetup.Org.Human.Username + "@" + orgDomain
}
mig.instanceSetup.Org.Human.Email.Address = mig.instanceSetup.Org.Human.Email.Address.Normalize()
if mig.instanceSetup.Org.Human.Email.Address == "" {
mig.instanceSetup.Org.Human.Email.Address = domain.EmailAddress(mig.instanceSetup.Org.Human.Username)
if !strings.Contains(string(mig.instanceSetup.Org.Human.Email.Address), "@") {
orgDomain, err := domain.NewIAMDomainName(mig.instanceSetup.Org.Name, mig.instanceSetup.CustomDomain)
if err != nil {
return err
}
mig.instanceSetup.Org.Human.Email.Address = domain.EmailAddress(mig.instanceSetup.Org.Human.Username + "@" + orgDomain)
}
}
_, token, key, loginClientToken, _, err := cmd.SetUpInstance(ctx, &mig.instanceSetup)
if err != nil {
return err
}
if (mig.instanceSetup.Org.Machine != nil &&
((mig.instanceSetup.Org.Machine.Pat != nil && token == "") ||
(mig.instanceSetup.Org.Machine.MachineKey != nil && key == nil))) ||
(mig.instanceSetup.Org.LoginClient != nil &&
(mig.instanceSetup.Org.LoginClient.Pat != nil && loginClientToken == "")) {
return err
}
return mig.outputMachineAuthentication(key, token, loginClientToken)
}
func (mig *FirstInstance) verifyEncryptionKeys(ctx context.Context) (*crypto_db.Database, error) {
keyStorage, err := crypto_db.NewKeyStorage(mig.db, mig.masterKey)
if err != nil {
return nil, fmt.Errorf("cannot start key storage: %w", err)
}
if err = verifyKey(ctx, mig.userEncryptionKey, keyStorage); err != nil {
return nil, err
}
if err = verifyKey(ctx, mig.smtpEncryptionKey, keyStorage); err != nil {
return nil, err
}
if err = verifyKey(ctx, mig.oidcEncryptionKey, keyStorage); err != nil {
return nil, err
}
return keyStorage, nil
}
func (mig *FirstInstance) outputMachineAuthentication(key *command.MachineKey, token, loginClientToken string) error {
if key != nil {
keyDetails, err := key.Detail()
if err != nil {
return err
}
if err := outputStdoutOrPath(mig.MachineKeyPath, string(keyDetails)); err != nil {
return err
}
}
if token != "" {
if err := outputStdoutOrPath(mig.PatPath, token); err != nil {
return err
}
}
if loginClientToken != "" {
if err := outputStdoutOrPath(mig.LoginClientPatPath, loginClientToken); err != nil {
return err
}
}
return nil
}
func outputStdoutOrPath(path string, content string) (err error) {
f := os.Stdout
if path != "" {
f, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer f.Close()
}
_, err = fmt.Fprintln(f, content)
return err
}
func (mig *FirstInstance) String() string {
return "03_default_instance"
}
func verifyKey(ctx context.Context, key *crypto.KeyConfig, storage crypto.KeyStorage) (err error) {
_, err = crypto.LoadKey(key.EncryptionKeyID, storage)
if err == nil {
return nil
}
k, err := crypto.NewKey(key.EncryptionKeyID)
if err != nil {
return err
}
return storage.CreateKeys(ctx, k)
}