1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
|
# Authenticating Workloads over TLS-encrypted HTTP Connections Using JWT-SVIDs
This example shows how to use the go-spiffe library to make a server workload authenticate a client workload using JWT-SVIDs fetched from the Workload API.
JWT-SVIDs are useful when the workloads are not able to establish an mTLS communication channel between each other. For instance, when the server workload is behind a TLS terminating load balancer or proxy, a client workload cannot be authenticated directly by the server via mTLS and X.509-SVID. So, an alternative is to forego authenticating the client at the load balancer or proxy and instead require that clients authenticate via SPIFFE JWT-SVIDs conveyed directly to the server via the application layer.
The scenario used in this example goes like this:
1. The server:
- Creates an X509Source struct.
- Creates a JWTSource struct.
- Starts listening for HTTP requests over TLS. Only one resource is served at `/`.
2. The reverse proxy:
- Creates an X509Source struct.
- Starts listening for HTTP requests over TLS. It forwards requests to `/` only.
3. The client:
- Creates an X509Source struct.
- Creates a JWTSource struct.
- Fetches a JWT-SVID using the JWTSource.
- Creates a `GET /` request with the JWT-SVID set as the value of the `Authorization` header.
- Sends the request to the proxy using TLS authentication for establishing the connection.
4. The proxy receives the request, logs the request's method and URL, and forwards the request to the server.
5. The server receives the request, extracts the JWT-SVID from the `Authorization` header, and verifies the token. If the token is valid, it logs `Request received` and returns a response with a body containing the string `Success!!!`, otherwise an `Unauthorized` HTTP code is returned.
6. The proxy receives the response from the server and passes it to the client.
7. The client receives the response. If the response has an HTTP 200 status, its body is logged, otherwise the HTTP status code is logged.
## Creating an X509Source struct
As you may noted, the three workloads create a [workloadapi.X509Source](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/workloadapi?tab=doc#X509Source) struct.
```go
x509Source, err := workloadapi.NewX509Source(
ctx,
workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath)),
)
```
Where:
- ctx is a `context.Context`. `NewX509Source` function blocks until the first Workload API response is received or this context times out or is cancelled.
- socketPath is the address of the Workload API (`unix:///tmp/agent.sock`) to which the internal Workload API client connects to get up-to-date SVIDs. Alternatively, we could have omitted this configuration option, in which case the listener would have used the `SPIFFE_ENDPOINT_SOCKET` environment variable to locate the Workload API. The code could have then been written like this:
```go
x509Source, err := workloadapi.NewX509Source(ctx)
```
In all cases, the `X509Source` is used to create a `tls.Config` for the underlying transport connection of the HTTP client/server. However, there are some differences in its usage on the server, client, and proxy workloads:
The **server workload** uses the `X509Source` to create the [TLSServerConfig](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig?tab=doc#TLSServerConfig) for the HTTP server used:
```go
server := &http.Server{
Addr: ":8080",
TLSConfig: tlsconfig.TLSServerConfig(x509Source),
}
```
This enables the server to present an X.509-SVID to the other end of the connection. This SVID is provided by the `X509Source` via the Workload API.
The **client workload** uses the `X509Source` to create the [TLSClientConfig](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig?tab=doc#TLSClientConfig) for the `Transport` of the HTTP client used:
```go
serverID := spiffeid.RequireFromString("spiffe://example.org/server")
.
.
.
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsconfig.TLSClientConfig(
x509Source,
tlsconfig.AuthorizeID(serverID),
),
},
}
```
This enables the client to verify that the X.509-SVID presented by the other end of the connection has the specified SPIFFE ID by using:
- The trust bundle provided by the Workload API via the `X509Source`.
- The [Authorizer](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig?tab=doc#Authorizer) returned by [tlsconfig.AuthorizeID()](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig?tab=doc#AuthorizeID)
The **proxy workload** uses the `X509Source` to create the [TLSClientConfig](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig?tab=doc#TLSClientConfig) for the `Transport` of the HTTP reverse proxy used:
```go
proxy := httputil.NewSingleHostReverseProxy(remote)
transport := *(http.DefaultTransport.(*http.Transport)) // copy of http.DefaultTransport.
transport.TLSClientConfig = tlsconfig.TLSClientConfig(
x509Source, tlsconfig.AuthorizeID(spiffeid.RequireFromString("spiffe://example.org/server")),
)
proxy.Transport = &transport
```
This enables the proxy to verify that the X.509-SVID presented by the server has the specified SPIFFE ID by using:
- The trust bundle provided by the Workload API via the `X509Source`.
- The [Authorizer](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig?tab=doc#Authorizer) function returned by [tlsconfig.AuthorizeID()](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig?tab=doc#AuthorizeID)
The **proxy workload** also uses the `X509Source` to create the [TLSServerConfig](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig?tab=doc#TLSServerConfig) for the HTTP server used:
```go
server := &http.Server{
Addr: ":8443",
TLSConfig: tlsconfig.TLSServerConfig(x509Source),
}
```
This enables the proxy to present an X.509-SVID to the client. This SVID is provided by the `X509Source` via the Workload API, and contains the SPIFFE ID of the server (as explained later in **Create the registration entries** section).
## Creating a JWTSource struct
On the scenario described we can see that only the client and the server workloads create a [workloadapi.JWTSource](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/workloadapi?tab=doc#JWTSource). This is because the proxy workload doesn't need to deal with JWTs since the server is the one in charge of authenticating the clients:
```go
jwtSource, err := workloadapi.NewJWTSource(
ctx,
workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath)),
)
```
Where:
- ctx is a `context.Context`. `NewJWTSource` function blocks until the first Workload API response is received or this context times out or is cancelled.
- socketPath is the address of the Workload API (`unix:///tmp/agent.sock`) to which the internal Workload API client connects to get up-to-date SVIDs. Alternatively, we could have omitted this configuration option, in which case the listener would have used the `SPIFFE_ENDPOINT_SOCKET` environment variable to locate the Workload API. The code could have then been written like this:
```go
jwtSource, err := workloadapi.NewJWTSource(ctx)
```
Although both client and server workloads create a `JWTSource`, it is used differently in each case:
The **client workload** uses the `JWTSource` to get a JWT-SVID by calling its [FetchJWTSVID](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/workloadapi?tab=doc#JWTSource.FetchJWTSVID) function:
```go
svid, err := jwtSource.FetchJWTSVID(ctx, jwtsvid.Params{
Audience: audience,
})
```
Where:
- ctx is a `context.Context`. `FetchJWTSVID` method blocks until a response is received or this context times out or is cancelled.
- audience is the intended recipient of the JWT-SVID. By default it is `spiffe://example.org/server`, otherwise it is equal to the value passed as the first argument of the client's executable.
Then, the client uses the JWT-SVID to set a bearer token to the request's `Authorization` header:
```go
req, err := http.NewRequest("GET", serverURL, nil)
.
.
.
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", svid.Marshal()))
```
The **server workload** uses the `JWTSource` to authenticate the client by calling the [ParseAndValidate](https://pkg.go.dev/github.com/spiffe/go-spiffe/v2/svid/jwtsvid?tab=doc#ParseAndValidate) function:
```go
_, err := jwtsvid.ParseAndValidate(token, a.jwtSource, a.audiences)
```
Where:
- token is the marshalled JWT-SVID sent by the client in the `Authorization` header.
- a.jwtSource is the `JWTSource`.
- a.audiences is a slice of strings. Specifies a list of expected audiences in the `aud` field of the token.
When `ParseAndValidate` returns an error, the server returns an `Unauthorized` status. Otherwise, the request continues normal processing.
## That is it!
As we can see the go-spiffe library allows your application avoiding to deal with the implementation details of the Workload API. You just create the SVID sources and then simply ask the library for what you need.
## Building
To build the client workload:
```bash
cd examples/spiffe-jwt-using-proxy/client
go build
```
To build the proxy workload:
```bash
cd examples/spiffe-jwt-using-proxy/proxy
go build
```
To build the server workload:
```bash
cd examples/spiffe-jwt-using-proxy/server
go build
```
## Running
This example assumes the following preconditions:
- There is a SPIRE Server and Agent up and running.
- There is a Unix workload attestor configured.
- The trust domain is `example.org`.
- The agent SPIFFE ID is `spiffe://example.org/host`.
- There are `server-workload` and `client-workload` users in the system.
### 1. Create the registration entries
Create two registration entries, one for the client workload and another for the server and proxy workloads:
Server:
```bash
./spire-server entry create -spiffeID spiffe://example.org/server \
-parentID spiffe://example.org/host \
-selector unix:user:server-workload
```
Client:
```bash
./spire-server entry create -spiffeID spiffe://example.org/client \
-parentID spiffe://example.org/host \
-selector unix:user:client-workload
```
We will use the `server-workload` user to run the proxy because we want it to be as transparent as possible to the client. By using this user, the proxy will get an SVID with the same SPIFFE ID as the server, due to the `unix:user:server-workload` selector used when registering the entry.
### 2. Start the server
Start the server with the `server-workload` user:
```bash
sudo -u server-workload ./server
```
### 3. Start the proxy
Start the proxy with the `server-workload` user:
```bash
sudo -u server-workload ./proxy
```
### 4. Run the client
Run the client with the `client-workload` user:
```bash
sudo -u client-workload ./client
```
For each component the logs would contain:
| Component| Log content (stdout) |
|----------|----------------------|
| Proxy | `GET /` |
| Server | `Request received` |
| Client | `Success!!!` |
To demonstrate a failure, we can run the client using a wrong audience as the first argument:
```
sudo -u client-workload ./client spiffe://example.org/some-other-server
401 Unauthorized
```
Given that the server expects its own SPIFFE ID as the audience value it will reject the token because of the audience's mismatch. Then server log would contain:
```
Invalid token: jwtsvid: expected audience in ["spiffe://example.org/server"] (audience=["spiffe://example.org/some-other-server"])
```
|