Skip to content

Add Windows Kerberos/Negotiate proxy authentication (SSPI) - #180

Open
smithjw wants to merge 7 commits into
samuong:masterfrom
smithjw:kerberos-windows
Open

Add Windows Kerberos/Negotiate proxy authentication (SSPI)#180
smithjw wants to merge 7 commits into
samuong:masterfrom
smithjw:kerberos-windows

Conversation

@smithjw

@smithjw smithjw commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Builds on #178 (macOS Kerberos) to add the Windows half of Negotiate proxy authentication, so a domain-joined Windows user gets the same automatic, no-flag Kerberos experience macOS already has.

What it does

  • Extracts the platform-neutral negotiateAuthenticator into kerberos_common.go (darwin || windows); each platform supplies just checkKerberosTicket() and generateSPNEGOToken().
  • Adds kerberos_windows.go, an SSPI backend via github.com/alexbrainman/sspi/negotiate (pure Go, no cgo). It requests the HTTP/<host> SPN that Active Directory registers.
  • Slots into the existing Negotiate -> NTLM -> Basic chain; --no-kerberos opts out. macOS behaviour is unchanged.

Testing

  • Unit tests for the Windows wiring and the credential probe (CI-safe).
  • An integration test (integration && windows) that self-skips unless it runs on a domain-joined host with a ticket, so it never fails in CI. It mirrors the macOS integration test's assertions.
  • Validated end-to-end against a reference environment (Windows 11 24H2 joined to a Samba AD domain, with a Negotiate-advertising Squid): the integration test passes 5/5 as a domain user holding a real AES-256 TGT, and the actual alpaca.exe (Negotiate-only, no NTLM/Basic configured) returns 200 through the proxy. testdata/kerberos-windows-integration/README.md documents how to reproduce it.

Also in here

  • Renames the macOS e2e integration test to a darwin-explicit name (integration && darwin, TestKerberosDarwinIntegration, testdata/kerberos-darwin-integration/), completing the rename discussed in Multi-method proxy auth with macOS Kerberos: Builds on #168 #178 and establishing one integration && <os> convention across both platforms. Adds the darwin unit test that the shared-code extraction would otherwise have dropped.
  • Renames CLAUDE.md -> AGENTS.md (a tool-neutral contributor guide).
  • Includes the OpenSpec planning docs under openspec/. Happy to remove these from the PR if we don't want them just yet - say the word and I'll strip them in a follow-up commit.

Notes

  • Windows support is labelled experimental: validated against one reference environment, not yet broadly tested against production AD and a range of proxies.
  • alexbrainman/sspi is pinned by hash in go.sum; happy to vendor it or request a tagged release if you'd prefer.
  • Two testing improvements raised in Multi-method proxy auth with macOS Kerberos: Builds on #168 #178 (a single canonical container image, and a real NTLM server) fit naturally on top of this and are left for a follow-up.

Comment on lines +237 to +251
// instrumentedBasic wraps a basicAuthenticator with a call counter so a test
// can assert Basic was not invoked when Negotiate should have won.
type instrumentedBasic struct {
*basicAuthenticator
calls atomic.Int32
}

func (b *instrumentedBasic) do(req *http.Request, rt http.RoundTripper) (*http.Response, error) {
b.calls.Add(1)
return b.basicAuthenticator.do(req, rt)
}

func newInstrumentedBasic(creds string) *instrumentedBasic {
return &instrumentedBasic{basicAuthenticator: newBasicAuthenticator(creds)}
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

this looks the same as the instrumentedBasic type in kerberos_darwin_integration_test.go. i wonder if this could go into basicauth_test.go, or maybe even just merged with basicAuthServer there? the basic auth tests could then have an additional assertion.

Comment thread kerberos.go
Comment on lines +20 to +21
// backend. Kerberos/Negotiate is implemented on macOS (GSS.framework,
// kerberos_darwin.go) and Windows (SSPI, kerberos_windows.go).

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

let's remove the second sentence of this comment, so that it doesn't churn every time we add a new platform?

Comment thread main.go
Comment on lines +134 to +138
// Kerberos/Negotiate is auto-detected on macOS (GSS.framework) and
// Windows (SSPI): if a valid ticket is present it is added to the
// chain, and applicableTo re-checks per 407 so a ticket that arrives
// later is honoured without a restart. No flag needed for the common
// already-signed-in case; pass --no-kerberos to opt out.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

i can already see a lot of code churn due to Claude constantly mentioning which operating systems are supported, over and over again. We don't need to talk about it in main.go; we can probably just delete this entire paragraph?

Comment thread kerberos_common.go
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build darwin || windows

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

the various kerberos files were a bit confusing to me at first glance, can we rename them to:

  1. kerberos.go -> kerberos_stub.go
  2. kerberos_common.go -> kerberos.go

Comment on lines +69 to +70
t.Skip("integration: ALPACA_IT_PROXY not set; run via the " +
"testdata/kerberos-windows-integration harness on a domain-joined host")

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

i'm a bit confused by what "run via the testdata/kerberos-windows-integration harness" means. is this just saying to run it with the right env vars and test tags set?

Comment on lines +19 to +26
// Unlike the macOS fixture, this test cannot create its own environment:
// SSPI requires the host to be domain-joined with a real Kerberos credential,
// which can't be containerised or run on the CI matrix. The test therefore
// reads its connection details from the environment and self-skips when they
// (or a real ticket) are absent, so it never fails on a developer machine or
// in CI. testdata/kerberos-windows-integration/README.md documents the
// domain-joined environment used to exercise it (a Samba AD DC and a
// Negotiate-advertising Squid) and how to reproduce it locally.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

It's a shame that we can't automate the harness setup for Windows, but it is what it is. The actual test logic (the sub-tests) themselves look more-or-less the same as Darwin though?

Is there a reason we can't have the test logic in some sort of platform-independent file (e.g. kerberos_integration_test.go) but it calls some platform-specific setup function (e.g. with separate implementations in kerberos_darwin_integration_test.go and kerberos_windows_integration_test.go)?

Alternatively, could the darwin test setup just set up the ALPACA_IT_* env vars and call these tests?

That way, we can reuse the tests for new platform implementations such as Linux.

@@ -0,0 +1,96 @@
# negotiate-authentication Specification

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Overall, I like the idea of SDD and I think we'll need to figure out a process for Alpaca, but I don't think we're quite there yet, so let's leave the specs out of this PR? Let's find some time to discuss AI-assisted development processes once this is merged.

For future reference, one of the things I think we need to do is ensure that specs reference any applicable RFCs (or other standards). We've seen a few times now (outside of this PR) cases where Claude has hallucinated and implemented protocol-violating behaviour, and getting the relevant RFCs into its context is the least we should to do try to avoid that.

Comment thread AGENTS.md
@@ -158,7 +163,7 @@ Both are enforced in CI.

### Style

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Something I'm noticing is that Claude is adding a lot of documentation in both markdown files and in comments, and often even repeating comments in multiple places. This is creating a few problems:

  1. General noise - the actual code that we're interested in is buried in comments (if we want to preserve the original intent, that's already in the spec files, so these code-level comments are redundant). This is distracting to humans like me, but I also imagine it is going to eat into the context window for coding agents too.
  2. Churn - e.g. every PR has to update all the comments that talk about which platforms are supported, and this makes even the diffs noisy. I mentioned this elsewhere in this PR review (as well as the previous one) but I might not have caught everything.

So there are a few things we should instruct coding agents to do, with regards to comments:

  • Prioritise writing readable, self-documenting code with descriptive variable and function names over adding comments.
  • Avoid writing comments that simply explain what the code is doing. Prefer writing comments to explain why a non-obvious approach was taken, such as working around a known library bug or optimising a critical bottleneck.
  • Avoid repeating the same information in multiple comments.
  • README.md contains all user-facing documentation. If there is detailed documentation for specific features that only apply to a subset of users, it should link to a file in the doc/ subdirectory.
  • Documentation should be as close to the code as possible. This means that code-level (or source-file-level) comments are preferred over markdown docs, unless there is too much information for a comment. In that case, the comment should reference a file in the doc/ subdirectory.

If you've got any other prompts, let's discuss adding them too. Are you able to ask Claude to remove comments based on these guidelines and see if and how much this helps?

Also I'm not sure where this belongs, maybe in another section...

  • Logs and error messages need to have enough detail to troubleshoot what went wrong.
  • Error messages also need to include information on how users can fix the error.

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.

2 participants