Skip to content

Conversation

@dealloc
Copy link

@dealloc dealloc commented Dec 1, 2025

Closes #1015

Adds host support for Zitadel to Aspire.

PR Checklist

  • Created a feature/dev branch in your fork (vs. submitting directly from a commit on main)
  • Based off latest main branch of toolkit
  • PR doesn't include merge commits (always rebase on top of our main, if needed)
  • New integration
    • Docs are written
    • Added description of major feature to project description for NuGet package (4000 total character limit, so don't push entire description over that)
  • Tests for the changes have been added (for bug fixes / features) (if applicable)
  • Contains NO breaking changes
  • Every new API (including internal ones) has full XML docs
  • Code follows all style conventions

Other information

Currently in draft since I haven't figured out everything.

  • HTTPS would be great if I could set this up by default, but I couldn't figure out how to get Aspire to share it's SSL
  • Zitadel requires the hostname to be known, if you access from service discovery it would be <name> but if you open from the dashboard it's localhost:<port>. Currently it uses a semi-hardcoded <name>.dev.localhost as a middle ground.

I played around with something similar to Keycloak's WithRealmImport to "seed" the instance but AFAIK Zitadel doesn't support such things.

I need to update some of the docs that are missing and write the tests, not sure if the example I've included is sufficient for now

@dealloc
Copy link
Author

dealloc commented Dec 1, 2025

@dotnet-policy-service agree

Copy link
Member

@aaronpowell aaronpowell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mentioned a problem with hostnames, can you tag the line(s) where that's surfacing?

@dealloc
Copy link
Author

dealloc commented Dec 3, 2025

You mentioned a problem with hostnames, can you tag the line(s) where that's surfacing?

Currently that would be https://github.com/CommunityToolkit/Aspire/pull/1021/files/677452ebe38d7be79caf11268057182f5594136d#diff-427aa76f92a729429b7b134c3be02f0a722e8ab12d85a50a2bc822d9819cda33R35

Zitadel requires a "stable" external hostname or it'll return a 404 if the Host header does not match what's configured.

This means that if I configure the external hostname as localhost and I would call it from Aspire's service discovery (e.g zitadel:<port>) it would return a 404 as it can't "match" to an instance. On the other hand setting it to the resource name would mean you can't open it in the browser from Aspire's dashboard.

Currently I'm setting it to <name>.dev.localhost which seemed a good middle ground but I can't seem to override the URL shown in the dashboard, and I'm not sure if this is the "idiomatic" way to handle this in Aspire either

@dealloc
Copy link
Author

dealloc commented Dec 3, 2025

rebased changes on main

@aaronpowell
Copy link
Member

Is there a reason that the host:port that Aspire will assign the container couldn't be provided for the external domain?

@dealloc
Copy link
Author

dealloc commented Dec 4, 2025

Is there a reason that the host:port that Aspire will assign the container couldn't be provided for the external domain?

How would I retrieve that?

@aaronpowell
Copy link
Member

Is there a reason that the host:port that Aspire will assign the container couldn't be provided for the external domain?

How would I retrieve that?

Use the endpoint reference and get the allocated endpoint, that will have all the parts of the endpoint on it

@dealloc
Copy link
Author

dealloc commented Dec 8, 2025

@aaronpowell one final tweak I'd like to make is that the Aspire dashboard generates the URL to the dashboard with localhost:<port> while Zitadel will expect the same hostname as passed to ZITADEL_EXTERNALDOMAIN (which now properly resolves to the endpoint's hostname like zitadel)

it's a minor thing as this only affects the link shown in the dashboard, but I feel it would be a lot more polished if I managed to get that working as well

@aaronpowell
Copy link
Member

I think I'm still not properly understanding the role of the EXTERNALDOMAIN environment variable.

The endpoint reference host is going to be pointing to the host that the resource is exposed within the container network, so zitadel in this case, which is how other containers can address it (which is how the zitadel resource talks to the PG database using postgres as the host).

Resources outside the network, say your browser, would use the host localhost and the port that was assigned via aspire, not zitadel:8080 since you're not in the same container network.

So then, shouldn't the EXTERNALDOMAIN be localhost? Looking at the sample docker-compose.yml that is what they have set.

@ahmedhemdan21
Copy link

I think I'm still not properly understanding the role of the EXTERNALDOMAIN environment variable.

The endpoint reference host is going to be pointing to the host that the resource is exposed within the container network, so zitadel in this case, which is how other containers can address it (which is how the zitadel resource talks to the PG database using postgres as the host).

Resources outside the network, say your browser, would use the host localhost and the port that was assigned via aspire, not zitadel:8080 since you're not in the same container network.

So then, shouldn't the EXTERNALDOMAIN be localhost? Looking at the sample docker-compose.yml that is what they have set.

Agreed, whenever I run without setting the external domain I get

unable to set instance using origin &{localhost:28080 http} (ExternalDomain is zitadel)
while adding WithExternalDomain("localhost") make it work without any issues.

@dealloc
Copy link
Author

dealloc commented Dec 9, 2025

I think I'm still not properly understanding the role of the EXTERNALDOMAIN environment variable.

The endpoint reference host is going to be pointing to the host that the resource is exposed within the container network, so zitadel in this case, which is how other containers can address it (which is how the zitadel resource talks to the PG database using postgres as the host).

Resources outside the network, say your browser, would use the host localhost and the port that was assigned via aspire, not zitadel:8080 since you're not in the same container network.

So then, shouldn't the EXTERNALDOMAIN be localhost? Looking at the sample docker-compose.yml that is what they have set.

A single Zitadel instance can have multiple "virtual" instances running and it uses the Host header on incoming requests to differentiate which instance to select.

This means if we set EXTERNALDOMAIN to localhost it won't "recognize" which instance to select if a resource calls zitadel (even though Aspire would resolve to the same service), which in turn results in Zitadel responding with the unable to set instance using origin &{localhost:28080 http} (ExternalDomain is zitadel) error

Let's assume the following setup in Aspire:

var builder = DistributedApplication.CreateBuilder(args);

var database = builder.AddPostgres("postgres");

builder.AddZitadel("zitadel")
    .WithDatabase(database);

builder.AddProject<Example>("my-example")
    .WithReference(zitadel);

builder.Build().Run();

if we set EXTERNALDOMAIN to localhost:

  • the user navigating in his browser to the instance would WORK (because the browser passes localhost as the hostname)
  • the example application (which would call zitadel) would NOT WORK because it sets the hostname as zitadel

As a middle ground I set zitadel.dev.localhost since it would work for both scenarios.

I hope that helps clarify the role of EXTERNALDOMAIN and the issue with localhost / <resourcename>

@danegsta
Copy link

@dealloc APIs to make the .NET dev cert available to resources (and configure them to use it) are in Aspire main and will ship with 13.1 as early as next week, so you'd be able to get HTTPS working once that's available.

@danegsta
Copy link

Also, as a heads up, *.internal addresses work reliably in every major browser, but support isn't consistent at the OS level. You can visit zitadel.dev.internal in a browser and it'll resolve correctly to localhost, but if you try to do the same outside a browser (say with curl) it won't always work. Depends on the particular system DNS setup.

.WithUrlForEndpoint(ZitadelResource.HttpEndpointName, e => e.DisplayText = "Zitadel Dashboard");

// Use ReferenceExpression for the port to avoid issues with endpoint allocation
var endpoint = resource.GetEndpoint(ZitadelResource.HttpEndpointName);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will get a reference to the endpoint from the perspective of the host, so it'll resolve the localhost domain and port instead of the internal container address and port. That being said there's a bug that'll need to be fixed that's preventing the network identifier from actually applying: dotnet/aspire#13440

Suggested change
var endpoint = resource.GetEndpoint(ZitadelResource.HttpEndpointName);
var endpoint = resource.GetEndpoint(ZitadelResource.HttpEndpointName, KnownNetworkIdentifiers.LocalhostNetwork);

Comment on lines +65 to +66
.WithEnvironment("ZITADEL_TLS_ENABLED", "false")
.WithEnvironment("ZITADEL_EXTERNALSECURE", "false")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once Aspire 13.1 is available, this will get you HTTPS support. It'll set the appropriate config if the resource is configured with a certificate (it'll get the ASP.NET development certificate by default):

Suggested change
.WithEnvironment("ZITADEL_TLS_ENABLED", "false")
.WithEnvironment("ZITADEL_EXTERNALSECURE", "false")
.WithEnvironment("ZITADEL_TLS_ENABLED", "false")
.WithEnvironment("ZITADEL_EXTERNALSECURE", "false")
.WithHttpsCertificateConfiguration(ctx =>
{
ctx.EnvironmentVariables["ZITADEL_EXTERNALSECURE"] = "true";
ctx.EnvironmentVariables["ZITADEL_TLS_ENABLED"] = "true";
ctx.EnvironmentVariables["ZITADEL_TLS_CERTPATH"] = ctx.CertificatePath;
ctx.EnvironmentVariables["ZITADEL_TLS_KEYPATH"] = ctx.KeyPath;
return Task.CompletedTask;
})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll also need something like this to optionally enable HTTPS on the endpoint. It's a bit boilerplate currently as we're still working on the API for updating endpoints:

if (builder.ExecutionContext.IsRunMode)
{
    builder.Eventing.Subscribe<BeforeStartEvent>((@event, cancellationToken) =>
    {
        var developerCertificateService = @event.Services.GetRequiredService<IDeveloperCertificateService>();

        bool addHttps = false;
        if (!zitadelBuilder.Resource.TryGetLastAnnotation<HttpsCertificateAnnotation>(out var annotation))
        {
            if (developerCertificateService.UseForHttps)
            {
                // If no certificate is configured, and the developer certificate service supports container trust,
                // configure the resource to use the developer certificate for its key pair.
                addHttps = true;
            }
        }
        else if (annotation.UseDeveloperCertificate.GetValueOrDefault(developerCertificateService.UseForHttps) || annotation.Certificate is not null)
        {
            addHttps = true;
        }

        if (addHttps)
        {
            // If a TLS certificate is configured, override the endpoint to use HTTPS instead of HTTP
            // Zitadel only binds to a single port
            zitadelBuilder
                .WithEndpoint(ZitadelResource.HttpEndpointName, ep => ep.UriScheme = "https");
        }

        return Task.CompletedTask;
    });
}

@danegsta
Copy link

Added a couple comments on how to get the endpoint reference to give you the correct ZITADEL_EXTERNALDOMAIN value and get HTTPS working (once 13.1 ships):

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hosting integration for Zitadel

4 participants