Skip to content

Commit 62736c6

Browse files
committed
spike: discovery server
1 parent 28d5960 commit 62736c6

File tree

16 files changed

+1846
-0
lines changed

16 files changed

+1846
-0
lines changed

discovery/GEMINI.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Discovery Service Project
2+
3+
## Overview
4+
This project implements a public discovery service designed for decentralized, secure peer discovery. The core innovation is the use of **Custom Certificate Authorities (CAs)** to define isolated "Universes". Clients register and discover peers within their own Universe, identified and secured purely by mTLS.
5+
6+
The service emulates a Kubernetes API, allowing interaction via `kubectl`, including support for **Server-Side Apply**.
7+
8+
## Key Concepts
9+
10+
### 1. The "Universe"
11+
- A **Universe** is an isolated scope for peer discovery.
12+
- It is cryptographically defined by the **SHA256 hash of the Root CA's Public Key**.
13+
- Any client possessing a valid certificate signed by a specific CA belongs to that CA's Universe.
14+
- Different CAs = Different Universes. There is no crossover.
15+
16+
### 2. Authentication & Authorization
17+
- **Mechanism**: Mutual TLS (mTLS).
18+
- **Client Identity**: Derived from the **Common Name (CN)** of the leaf certificate.
19+
- **Universe Context**: Derived from the **Root CA** presented in the TLS handshake.
20+
- **Requirement**: Clients **MUST** present the full certificate chain (Leaf + Root CA) during the handshake. The server does not maintain a pre-configured trust store for these custom CAs; it uses the presented chain to determine the scope.
21+
22+
### 3. API Resources
23+
- **DiscoveryEndpoint** (`discovery.kops.k8s.io/v1alpha1`): Represents a peer in the discovery network. Can optionally hold OIDC configuration (Issuer URL, JWKS).
24+
- **Validation**: A client with CN `client1` can only Create/Update a `DiscoveryEndpoint` named `client1`.
25+
- **Apply Support**: The server supports `PATCH` requests to facilitate `kubectl apply --server-side`.
26+
27+
### 4. OIDC Discovery
28+
The server acts as an OIDC Discovery Provider for the Universe.
29+
- **Public Endpoints**:
30+
- `/.well-known/openid-configuration`: Returns the OIDC discovery document.
31+
- `/openid/v1/jwks`: Returns the JSON Web Key Set (JWKS).
32+
- **Data Source**: These endpoints serve data uploaded by clients via the `DiscoveryEndpoint` resource.
33+
34+
## Architecture
35+
36+
### Project Structure
37+
- `cmd/discovery-server/`: Main entry point. Wires up the HTTP server with TLS configuration.
38+
- `pkg/discovery/`:
39+
- `auth.go`: logic for inspecting TLS `PeerCertificates` to extract the Universe ID (CA hash) and Client ID.
40+
- `store.go`: In-memory thread-safe storage (`MemoryStore`) mapping Universe IDs to lists of `DiscoveryEndpoint` objects.
41+
- `server.go`: HTTP handlers implementing the K8s API emulation for `/apis/discovery.kops.k8s.io/v1alpha1`.
42+
- `k8s_types.go`: Definitions of `DiscoveryEndpoint`, `DiscoveryEndpointList`, `TypeMeta`, `ObjectMeta` etc.
43+
44+
### Data Model
45+
- **DiscoveryEndpoint**: The core resource. Contains `Spec.Addresses` and metadata.
46+
- **Universe**: Contains a map of `DiscoveryEndpoint` objects (keyed by name).
47+
- **Unified Types**: The API type `DiscoveryEndpoint` is used directly for in-memory storage, ensuring zero conversion overhead.
48+
49+
## Security Model
50+
- **Trust Delegation**: The server delegates trust to the CA. If you hold the CA key, you control the Universe.
51+
- **Isolation**: The server ensures that a client presenting a cert chain for `CA_A` cannot read or write data to the Universe defined by `CA_B`.
52+
- **Ephemeral**: The current implementation uses in-memory storage. Data is lost on restart.
53+
54+
## Building and Running
55+
56+
### Build
57+
```bash
58+
go build ./cmd/discovery-server
59+
```
60+
61+
### Run
62+
63+
See docs/walkthrough.md for instructions on testing functionality.

discovery/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Discovery Service
2+
3+
A public discovery service using mTLS for authentication and "Universe" isolation, emulating a Kubernetes API.
4+
5+
## Concept
6+
7+
- **Universe**: Defined by the SHA256 Fingerprint of a Custom CA Certificate.
8+
- **Client**: Identified by a Client Certificate signed by that Custom CA.
9+
- **DiscoveryEndpoint**: The resource type representing a registered client.
10+
- **Isolation**: Clients can only see `DiscoveryEndpoint` objects signed by the same Custom CA.
11+
12+
## Usage
13+
14+
### Run Server
15+
16+
```bash
17+
go run ./cmd/discovery-server --tls-cert server.crt --tls-key server.key --listen :8443
18+
```
19+
20+
(You can generate a self-signed server certificate for testing, see the [walkthrough](docs/walkthrough.md) ).
21+
22+
### Client Requirement
23+
24+
Clients must authenticate using mTLS.
25+
**Important**: The client MUST provide the full certificate chain, including the Root CA, because the server does not have pre-configured trust stores for these custom universes.
26+
The server identifies the Universe from the SHA256 hash of the Root CA certificate found in the TLS chain.
27+
28+
### Quick start
29+
30+
See `docs/walkthrough.md` for detailed instructions.
31+
32+
33+
## OIDC Discovery
34+
35+
The discovery server also serves OIDC discovery information publicly, allowing external systems (like AWS IAM) to discover the cluster's identity provider configuration.
36+
37+
- `GET /<universe-id>/.well-known/openid-configuration`: Returns the OIDC discovery document.
38+
- `GET /<universe-id>/openid/v1/jwks`: Returns the JWKS.
39+
40+
This information is populated by clients uploading `DiscoveryEndpoint` resources containing the `oidc` spec.
41+
42+
## Building and Running
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package v1alpha1
2+
3+
import (
4+
"k8s.io/apimachinery/pkg/runtime/schema"
5+
metav1 "k8s.io/kops/discovery/apis/meta/v1"
6+
)
7+
8+
var DiscoveryEndpointGVR = schema.GroupVersionResource{
9+
Group: "discovery.kops.k8s.io",
10+
Version: "v1alpha1",
11+
Resource: "discoveryendpoints",
12+
}
13+
14+
var DiscoveryEndpointGVK = schema.GroupVersionKind{
15+
Group: "discovery.kops.k8s.io",
16+
Version: "v1alpha1",
17+
Kind: "DiscoveryEndpoint",
18+
}
19+
20+
// DiscoveryEndpoint represents a registered client in the discovery service.
21+
type DiscoveryEndpoint struct {
22+
metav1.TypeMeta `json:",inline"`
23+
metav1.ObjectMeta `json:"metadata,omitempty"`
24+
25+
Spec DiscoveryEndpointSpec `json:"spec,omitempty"`
26+
}
27+
28+
// DiscoveryEndpointSpec corresponds to our internal Node data.
29+
type DiscoveryEndpointSpec struct {
30+
Addresses []string `json:"addresses,omitempty"`
31+
LastSeen string `json:"lastSeen,omitempty"`
32+
OIDC *OIDCSpec `json:"oidc,omitempty"`
33+
}
34+
35+
type OIDCSpec struct {
36+
IssuerURL string `json:"issuerURL,omitempty"`
37+
Keys []JSONWebKey `json:"keys,omitempty"`
38+
}
39+
40+
type JSONWebKey struct {
41+
Use string `json:"use,omitempty"`
42+
KeyType string `json:"kty,omitempty"`
43+
KeyID string `json:"kid,omitempty"`
44+
Algorithm string `json:"alg,omitempty"`
45+
N string `json:"n,omitempty"`
46+
E string `json:"e,omitempty"`
47+
// Crv string `json:"crv,omitempty"`
48+
// X string `json:"x,omitempty"`
49+
// Y string `json:"y,omitempty"`
50+
}
51+
52+
// DiscoveryEndpointList is a list of DiscoveryEndpoint objects.
53+
type DiscoveryEndpointList struct {
54+
metav1.TypeMeta `json:",inline"`
55+
// Standard list metadata.
56+
// We implement a minimal subset.
57+
Metadata metav1.ListMeta `json:"metadata,omitempty"`
58+
Items []DiscoveryEndpoint `json:"items"`
59+
}

discovery/apis/meta/v1/types.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package v1
2+
3+
// TypeMeta describes an individual object in an API response or request
4+
// with strings representing the type of the object and its API schema version.
5+
// Structures that are versioned or persisted should inline TypeMeta.
6+
type TypeMeta struct {
7+
Kind string `json:"kind,omitempty"`
8+
APIVersion string `json:"apiVersion,omitempty"`
9+
}
10+
11+
// ObjectMeta is metadata that all persisted resources must have, which includes all objects
12+
// users must create.
13+
type ObjectMeta struct {
14+
Name string `json:"name,omitempty"`
15+
Namespace string `json:"namespace,omitempty"`
16+
UID string `json:"uid,omitempty"`
17+
ResourceVersion string `json:"resourceVersion,omitempty"`
18+
CreationTimestamp string `json:"creationTimestamp,omitempty"`
19+
Labels map[string]string `json:"labels,omitempty"`
20+
Annotations map[string]string `json:"annotations,omitempty"`
21+
}
22+
23+
type ListMeta struct {
24+
ResourceVersion string `json:"resourceVersion,omitempty"`
25+
Continue string `json:"continue,omitempty"`
26+
}
27+
28+
// APIResourceList is used for discovery
29+
type APIResourceList struct {
30+
TypeMeta `json:",inline"`
31+
GroupVersion string `json:"groupVersion"`
32+
Resources []APIResource `json:"resources"`
33+
}
34+
35+
type APIResource struct {
36+
Name string `json:"name"`
37+
SingularName string `json:"singularName"`
38+
Namespaced bool `json:"namespaced"`
39+
Kind string `json:"kind"`
40+
Verbs []string `json:"verbs"`
41+
}
42+
43+
// APIGroupList is used for root discovery (GET /apis).
44+
type APIGroupList struct {
45+
TypeMeta `json:",inline"`
46+
Groups []APIGroup `json:"groups"`
47+
}
48+
49+
type APIGroup struct {
50+
Name string `json:"name"`
51+
Versions []GroupVersionForDiscovery `json:"versions"`
52+
PreferredVersion GroupVersionForDiscovery `json:"preferredVersion"`
53+
}
54+
55+
type GroupVersionForDiscovery struct {
56+
GroupVersion string `json:"groupVersion"`
57+
Version string `json:"version"`
58+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"crypto/tls"
21+
"flag"
22+
"fmt"
23+
"log"
24+
"net/http"
25+
"os"
26+
27+
"k8s.io/kops/discovery/pkg/discovery"
28+
)
29+
30+
func main() {
31+
certFile := flag.String("tls-cert", "", "Path to server TLS certificate")
32+
keyFile := flag.String("tls-key", "", "Path to server TLS key")
33+
addr := flag.String("listen", ":8443", "Address to listen on")
34+
storageType := flag.String("storage", "memory", "Storage backend (memory, gcs)")
35+
flag.Parse()
36+
37+
if *certFile == "" || *keyFile == "" {
38+
fmt.Fprintf(os.Stderr, "Error: --tls-cert and --tls-key are required\n")
39+
flag.Usage()
40+
os.Exit(1)
41+
}
42+
43+
var store discovery.Store
44+
45+
switch *storageType {
46+
case "memory":
47+
store = discovery.NewMemoryStore()
48+
default:
49+
log.Fatalf("Unknown storage type: %s", *storageType)
50+
}
51+
52+
handler := discovery.NewServer(store)
53+
54+
tlsConfig := &tls.Config{
55+
ClientAuth: tls.RequestClientCert,
56+
// We do not set ClientCAs because we accept any CA and use it to define the universe.
57+
MinVersion: tls.VersionTLS12,
58+
}
59+
60+
server := &http.Server{
61+
Addr: *addr,
62+
Handler: handler,
63+
TLSConfig: tlsConfig,
64+
}
65+
66+
log.Printf("Discovery server listening on %s using %s storage", *addr, *storageType)
67+
if err := server.ListenAndServeTLS(*certFile, *keyFile); err != nil {
68+
log.Fatalf("Server failed: %v", err)
69+
}
70+
}

discovery/docs/walkthrough.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Walkthrough of functionality
2+
3+
4+
Quick start:
5+
6+
```bash
7+
# Generate certs, kubeconfig, yaml files
8+
# Check out the script to better understand how all the pieces fit together!
9+
./scripts/create-kubeconfig.sh
10+
11+
# Start Server (using generated server certs)
12+
go run ./cmd/discovery-server --tls-cert walkthrough/server.crt --tls-key walkthrough/server.key &
13+
14+
# Verify server is running and serving the DiscoveryEndpoint resource
15+
kubectl --kubeconfig=walkthrough/universe1/client1.kubeconfig api-resources
16+
17+
# List DiscoveryEndpoints
18+
kubectl --kubeconfig=walkthrough/universe1/client1.kubeconfig get discoveryendpoints --all-namespaces
19+
20+
# Register (Apply)
21+
# The `metadata.name` MUST match the Common Name (CN) of your client certificate (e.g., `client1`), or the server will reject it with 403 Forbidden.
22+
kubectl --kubeconfig=walkthrough/universe1/client1.kubeconfig apply -f walkthrough/universe1/client1-discoveryendpoint.yaml --server-side=true --validate=false
23+
24+
# List DiscoveryEndpoints
25+
kubectl --kubeconfig=walkthrough/universe1/client1.kubeconfig get discoveryendpoints --all-namespaces
26+
```
27+
28+
29+
## Using curl
30+
31+
The kubernetes API is a well-structured REST API, so we don't have to use kubectl.
32+
33+
If you want to test the API with curl, you must include the **Universe ID** in the URL path.
34+
35+
**Export the Universe ID:**
36+
```bash
37+
export UNIVERSE_ID=$(openssl x509 -in walkthrough/universe1/ca.crt -noout -fingerprint -sha256 | sed 's/SHA256 Fingerprint=//' | sed 's/://g' | tr '[:upper:]' '[:lower:]')
38+
echo "UNIVERSE_ID is ${UNIVERSE_ID}"
39+
```
40+
41+
```bash
42+
curl --cert walkthrough/universe1/client1-bundle.crt --key walkthrough/universe1/client1.key --cacert walkthrough/server.crt \
43+
"https://localhost:8443/${UNIVERSE_ID}/apis/discovery.kops.k8s.io/v1alpha1/namespaces/default/discoveryendpoints"
44+
```
45+
46+
## OIDC discovery
47+
48+
The goal here is to allow anonymous access to OIDC endpoints, so let's verify that:
49+
50+
**Getting the OpenID Configuration**
51+
```bash
52+
curl --cacert walkthrough/server.crt "https://localhost:8443/${UNIVERSE_ID}/.well-known/openid-configuration"
53+
```
54+
55+
**Getting the JWKS keys**
56+
```bash
57+
curl --cacert walkthrough/server.crt "https://localhost:8443/${UNIVERSE_ID}/openid/v1/jwks"
58+
```
59+
60+
Note that we do not need a client certificate to get this data. Data from the DiscoveryEndpoints is published publicly.

discovery/go.mod

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
module k8s.io/kops/discovery
2+
3+
go 1.25.4
4+
5+
require (
6+
k8s.io/apimachinery v0.34.3
7+
k8s.io/client-go v0.34.3
8+
k8s.io/klog/v2 v2.130.1
9+
)
10+
11+
require (
12+
github.com/davecgh/go-spew v1.1.1 // indirect
13+
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
14+
github.com/go-logr/logr v1.4.3 // indirect
15+
github.com/gogo/protobuf v1.3.2 // indirect
16+
github.com/json-iterator/go v1.1.12 // indirect
17+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
18+
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
19+
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
20+
github.com/x448/float16 v0.8.4 // indirect
21+
go.yaml.in/yaml/v2 v2.4.2 // indirect
22+
golang.org/x/net v0.38.0 // indirect
23+
golang.org/x/oauth2 v0.27.0 // indirect
24+
golang.org/x/sys v0.31.0 // indirect
25+
golang.org/x/term v0.30.0 // indirect
26+
golang.org/x/text v0.23.0 // indirect
27+
golang.org/x/time v0.9.0 // indirect
28+
gopkg.in/inf.v0 v0.9.1 // indirect
29+
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
30+
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
31+
sigs.k8s.io/randfill v1.0.0 // indirect
32+
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
33+
sigs.k8s.io/yaml v1.6.0 // indirect
34+
)

0 commit comments

Comments
 (0)