Skip to content

Commit 1d993df

Browse files
committed
docs: auth blog post
1 parent ec77ace commit 1d993df

10 files changed

+316
-14
lines changed

TODO.md

+6-13
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,13 @@
11
# TODO
22

33
- Panic handling needs some help. The server doesn't shut down, which is good -
4-
but it also doesn't disconnect, or tell the user anything - which is bad.
5-
There's also zero info output on panic using the dev dashboard.
4+
but it doesn't tell the user anything - which is bad. There's also zero info
5+
output on panic using the dev dashboard.
66

77
- Implement multi-cluster.
88

99
## Documentation
1010

11-
- Getting started needs help, in particular:
12-
- Granting your user should probably go before the install instructions.
13-
- Say something about the error when you don't have authorization.
14-
- Make the getting started on a real cluster instructions more clear. In
15-
particular, it seems like it is a little difficult to see the install commands
16-
and realize that's what you need to use.
17-
1811
## Authorization
1912

2013
- Groups are probably what most users are going to want to use to configure all
@@ -53,6 +46,9 @@
5346
- Does it make sense to do the `nsenter` trick for some use cases? This
5447
requires privileged mode to work.
5548

49+
- This is waiting on the next release of russh as `handle.open_channel_agent`
50+
just landed.
51+
5652
- There's some kind of lag happening when scrolling aggressively (aka, holding
5753
down a cursor). It goes fine for ~10 items and then has a hitch in the
5854
rendering.
@@ -77,16 +73,13 @@
7773
- Dashboard as a struct doesn't really make sense anymore, it should likely be
7874
converted over to a simple function.
7975

80-
- The initial coalesce in `Apex` is a little weird because of the initial
81-
loading screen - feels like it is jumping a couple frames.
82-
8376
- Move YAML over to viewport. Should viewport be doing syntax highlighting by
8477
default? How do we do a viewport over a set of lines that require history to
8578
do highlighting?
8679

8780
- There's a bug somewhere in `log_stream`. My k3d cluster restarted and while I
8881
could get all the logs, the stream wouldn't keep running - it'd terminate
89-
immediately. `stern` seemed to be working fine. Recreating the cluster caused
82+
immediately. `stern` seemed to be working fine. Recreating the cluster causedx
9083
everything to work again.
9184

9285
- Move over to something like

docs/components/blog.tsx

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import Link from 'next/link'
2+
import { getPagesUnderRoute } from 'nextra/context'
3+
import NextLink from 'next/link'
4+
import { useRouter } from 'next/router'
5+
import { Cards } from 'nextra/components'
6+
import clsx from 'clsx'
7+
import { link } from 'fs'
8+
9+
const Blog = () => {
10+
const { asPath } = useRouter()
11+
12+
let linkClasses = [
13+
'border',
14+
'border-zinc-200',
15+
'dark:border-[#414141]',
16+
'p-8',
17+
'lg:p-12',
18+
'bg-white',
19+
'dark:bg-neutral-800',
20+
'rounded-none',
21+
'hover:!border-primary',
22+
'hover:dark:bg-neutral-700/50',
23+
'hover:border-violet-300',
24+
'hover:shadow-2xl',
25+
'hover:shadow-primary/10',
26+
'dark:shadow-none',
27+
'transition-colors',
28+
'flex',
29+
'flex-col',
30+
]
31+
32+
const items = getPagesUnderRoute('/blog').map(
33+
({ route, frontMatter: { title, date, byline } }) => (
34+
<Link href={route} className={clsx(linkClasses)}>
35+
<div className="font-extrabold text-xl md:text-3xl text-balance">
36+
{title}
37+
</div>
38+
<div className="opacity-50 text-sm my-7 flex gap-2">
39+
<time dateTime={date.toISOString()}>
40+
{date.toLocaleDateString('en', {
41+
month: 'long',
42+
day: 'numeric',
43+
year: 'numeric',
44+
})}
45+
</time>
46+
<span className="border-r border-gray-500" />
47+
<span>by {byline}</span>
48+
</div>
49+
<span className="text-primary block font-bold mt-auto">
50+
Read more →
51+
</span>
52+
</Link>
53+
)
54+
)
55+
56+
return (
57+
<div className="container grid md:grid-cols-2 gap-7 pb-10 pt-10">
58+
{items}
59+
</div>
60+
)
61+
}
62+
63+
export default Blog

docs/components/index.tsx

Whitespace-only changes.

docs/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"@iconify-json/material-symbols": "^1.2.1",
2626
"@iconify-json/mdi": "^1.2.0",
2727
"@theguild/remark-mermaid": "^0.1.2",
28+
"clsx": "^2.1.1",
2829
"next": "^14.2.9",
2930
"next-themes": "^0.3.0",
3031
"nextra": "alpha",

docs/pages/_meta.js

+11
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ export default {
44
breadcrumb: false,
55
},
66
},
7+
blog: {
8+
type: 'page',
9+
title: 'Blog',
10+
theme: {
11+
layout: 'raw',
12+
typesetting: 'article',
13+
timestamp: false,
14+
breadcrumb: true,
15+
pagination: false,
16+
},
17+
},
718
index: {
819
title: 'Overview',
920
display: 'hidden',

docs/pages/blog.mdx

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import Blog from 'components/blog'
2+
3+
<Blog />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
---
2+
title: 'Stop Making Kubernetes Auth Hard'
3+
date: 2024-09-19
4+
byline: Thomas Rampelberg
5+
---
6+
7+
I've spent most of my time working with Kubernetes being afraid of auth. I
8+
understand how RBAC works and I know that `.kube/config` is what's required to
9+
talk to an API server, but that's pretty much where my understanding stopped.
10+
Configuring the API server to use an auth plugin, getting tokens or certificates
11+
and setting up plugins on my (and all my ueser's) laptops made me think that it
12+
was all a monumental task. Just getting my environment setup correctly _was_ a
13+
monumental task. Well, as part of implementing kty's oauth support, I've been
14+
forced to figure out how it all actually works. And, it turns out, it doesn't
15+
need to be nearly as complex as I thought it was.
16+
17+
## TLDR
18+
19+
Use OpenID and grant groups or users the correct permissions in your cluster. It
20+
is likely that your organization already has an OpenID provider in place.
21+
Google, Github, Okta can all be used (and many more). That's it, that's all you
22+
need. Don't bother with IAM, service accounts or any of that other stuff. Those
23+
are all reasonable for machines - not for users.
24+
25+
If you'd like to see how easy it is to get running, check out the
26+
[getting started guide](/getting-started). kty uses OpenID to verify your
27+
identity over SSH and then takes care of the rest for you.
28+
29+
Make sure to get it going with `kubectl` as well. Check out
30+
[kubelogin](https://github.com/int128/kubelogin?tab=readme-ov-file), a `kubectl`
31+
plugin that will do the OIDC dance for you. Note that if you can't make the
32+
modifications required for the API server, you'll want to use an
33+
[oidc-proxy](https://github.com/TremoloSecurity/kube-oidc-proxy). Luckily, most
34+
Kubernetes solutions support OIDC out of the box like [EKS][eks-oidc] or
35+
[GKE][gke-oidc].
36+
37+
[eks-oidc]:
38+
https://docs.aws.amazon.com/eks/latest/userguide/authenticate-oidc-identity-provider.html
39+
[gke-oidc]: https://cloud.google.com/kubernetes-engine/docs/how-to/oidc
40+
41+
Now, if you're interested in how it all works and would like to start being
42+
comfortable with how it all works, read on.
43+
44+
## Authentication
45+
46+
Let's start out by splitting "auth" into two parts: authentication and
47+
authorization. Authentication is how you prove who you are. The result of the
48+
authentication process is an identity that can be used to see what you are, or
49+
aren't authorized to do. If we didn't actually care about verifying your
50+
identity, authentication could be nothing more than sending the username in
51+
cleartext to the API server. Obviously, we'd like a solution that is a little
52+
bit more secure than that.
53+
54+
Kubernetes has a [whole bunch][auth-plugins] of ways to authenticate. Because it
55+
is the easiest to understand, let's start wit the static token file. This is
56+
equivalent to having a username as password. You put the token aka "password"
57+
into the file and then associate it with a username. If this sounds like
58+
`/etc/passwd`, that's because it is! Each request sent to the API server
59+
contains your token as a header. The API server looks up the token in its file
60+
and maps that to a user or set of groups. Very similar to sending the username
61+
to the API server, but now we've got a piece of shared data, the token, that
62+
verifies the identity.
63+
64+
Open ID Connect (OIDC) gets rid of the pre-shared secret and instead uses some
65+
cryptography magic to do the same thing. This allows for identity to be created
66+
in a central location and subsequently verified by anyone. When you authenticate
67+
with an OIDC provider, the end result of the process is an [ID
68+
token][oidc-id-token].
69+
70+
The ID token is a JSON web token (JWT) that contains a bunch of information
71+
about your identity. This is signed by the provider and can be verified by
72+
anyone with the public key. Most importantly, OIDC providers publish their
73+
configuration so that anyone can verify the token. If you're interested in
74+
what's in that configuration, check out [kty's][odic-config].
75+
76+
With an ID token and the way to verify it in hand, the API server can extract an
77+
identity from the token and use that as part of RBAC to understand what you're
78+
allowed to do. The association between the token and either groups or users
79+
happens as part of claims. If you've got a JWT, you can see the claims in your
80+
token by going to [jwt.io](https://jwt.io) and pasting it in. Here's a token
81+
that I've gotten for kty.
82+
83+
```JSON
84+
{
85+
"iss": "https://kty.us.auth0.com/",
86+
"aud": "P3g7SKU42Wi4Z86FnNDqfiRtQRYgWsqx",
87+
"iat": 1726784050,
88+
"exp": 1726820050,
89+
"sub": "github|123456",
90+
"email": "me@my-domain.com"
91+
}
92+
```
93+
94+
For this token, we could configure the API server to map the `email` claim to a
95+
user. This is just like the token file from above! Instead of using the
96+
pre-shared secret as the mapping, we've used the public key from the OIDC
97+
provider.
98+
99+
[auth-plugins]:
100+
https://kubernetes.io/docs/reference/access-authn-authz/authentication/
101+
[oidc-id-token]: https://auth0.com/docs/secure/tokens/id-tokens
102+
[oidc-config]: https://kty.us.auth0.com/.well-known/openid-configuration
103+
104+
## Authorization
105+
106+
Here's where it gets interesting. RBAC doesn't care about how you authenticated.
107+
If the API says you're a user - then you are that user. All it cares about is
108+
your identity and what roles that identity is bound to. Let's look at a simple
109+
role:
110+
111+
```yaml
112+
apiVersion: rbac.authorization.k8s.io/v1
113+
kind: ClusterRole
114+
metadata:
115+
name: view
116+
rules:
117+
- apiGroups:
118+
- ''
119+
resources:
120+
- pods
121+
verbs:
122+
- get
123+
- list
124+
- watch
125+
```
126+
127+
Any identity that is bound to this role can get, list or watch pods in any
128+
namespace. How does an identity get associated with this role? That's where the
129+
`ClusterRoleBinding` comes into play.
130+
131+
```yaml
132+
apiVersion: rbac.authorization.k8s.io/v1
133+
kind: ClusterRoleBinding
134+
metadata:
135+
name: view
136+
roleRef:
137+
apiGroup: rbac.authorization.k8s.io
138+
kind: ClusterRole
139+
name: view
140+
subjects:
141+
- apiGroup: rbac.authorization.k8s.io
142+
kind: User
143+
name: me@my-domain.com
144+
```
145+
146+
Assuming that we're still talking about the token from above, this role binding
147+
associates all the permissions in the `view` role with the user
148+
`me@my-domain.com`. That's it! We've authenticated the identity and then
149+
verified that it can do some actions on the cluster. As RBAC is opt-in, you need
150+
to be granted permissions to do anything. There are some policies that come by
151+
default, in fact the `view` cluster role is one that comes out of the box (but
152+
simplified in this example). To see what can be granted, make sure to check out
153+
the [documentation][rbac].
154+
155+
For extra credit, you can also bind roles to groups. We can configure a claim
156+
from the JWT to be a group in addition to the email address. Imagine granting
157+
permissions on a cluster based on which teams a user is a part of. In fact, you
158+
can map almost anything from someone's Github profile directly over to a group.
159+
This way, you can setup permissions once and manage membership entirely through
160+
your OIDC provider. When using groups, the role binding ends up looking a little
161+
different:
162+
163+
```yaml
164+
apiVersion: rbac.authorization.k8s.io/v1
165+
kind: ClusterRoleBinding
166+
metadata:
167+
name: view
168+
roleRef:
169+
apiGroup: rbac.authorization.k8s.io
170+
kind: ClusterRole
171+
name: view
172+
subjects:
173+
- apiGroup: rbac.authorization.k8s.io
174+
kind: Group
175+
name: my-team
176+
```
177+
178+
[rbac]: https://kubernetes.io/docs/reference/access-authn-authz/rbac/
179+
180+
## Bringing it Together
181+
182+
So, what does this all mean? Well, it means that we've now got a central
183+
location to manage access to our cluster. If you're using groups, membership
184+
when the token is granted is mapped to a role binding that grants exactly what
185+
someone needs to work with your cluster. The IDs can be user friendly, so you
186+
can read through the `RoleBinding` YAML to understand what's allowed or not. If
187+
you're using `kty`, you don't even need any plugins or configuration! Your users
188+
can use `ssh` and immediately get access to the cluster.
189+
190+
Please don't be afraid of auth! Don't continue to use incredibly complex systems
191+
that are hard to setup and/or easy to break. Say no to services that require
192+
blanket permissions like the Kubernetes dashboard. Use OIDC and make sure that
193+
users have exactly the permissions they need.

docs/pages/blog/_meta.tsx

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useConfig } from 'nextra-theme-docs'
2+
import NextLink from 'next/link'
3+
4+
export default {
5+
'*': {
6+
display: 'hidden',
7+
theme: {
8+
sidebar: false,
9+
timestamp: true,
10+
layout: 'default',
11+
topContent: function TopContent() {
12+
const { frontMatter } = useConfig()
13+
const { title, byline } = frontMatter
14+
const date = new Date(frontMatter.date)
15+
16+
return (
17+
<>
18+
<h1 className="text-balance">{title}</h1>
19+
<div className="text-gray-500 text-center">
20+
<time dateTime={date.toISOString()}>
21+
{date.toLocaleDateString('en', {
22+
month: 'long',
23+
day: 'numeric',
24+
year: 'numeric',
25+
})}
26+
</time>{' '}
27+
by {byline}
28+
</div>
29+
</>
30+
)
31+
},
32+
},
33+
},
34+
}

docs/tailwind.config.js

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ module.exports = {
55
'./components/**/*.{js,jsx,ts,tsx,md,mdx}',
66
],
77
theme: {
8+
container: {
9+
center: true,
10+
},
811
extend: {},
912
},
1013
plugins: [],

docs/tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"moduleResolution": "node",
1414
"resolveJsonModule": true,
1515
"isolatedModules": true,
16-
"jsx": "preserve"
16+
"jsx": "preserve",
17+
"baseUrl": "."
1718
},
1819
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
1920
"exclude": ["node_modules"]

0 commit comments

Comments
 (0)