mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
In order for the action to pass various RFDs were updated to fix misspellings, and the dictionary was expanded.
665 lines
27 KiB
Markdown
665 lines
27 KiB
Markdown
---
|
|
authors: Noah Stride ([email protected])
|
|
state: Implemented (16.2.0)
|
|
---
|
|
|
|
# RFD 00162 - Improving the on-prem fleet Bot management experience
|
|
|
|
## Required approvers
|
|
|
|
- Engineering: @zmb3
|
|
- Product: @klizhentas || @xinding33
|
|
- Security: @reedloden || @jentfoo
|
|
|
|
## What
|
|
|
|
Terminology:
|
|
|
|
- Bot: An identity within Teleport intended for use by machines as opposed to
|
|
humans. Many individual machines may act as this shared identity.
|
|
- `tbot`: The Teleport binary that acts as a Bot and generates credentials for
|
|
consumption by client applications.
|
|
- Bot instance: A single instance of `tbot` running on a host.
|
|
|
|
This RFD proposes improvements to the management of fleets of Machine ID Bots.
|
|
These improvements are mostly targeted at on-prem deployments, where the
|
|
delegated join methods are not available.
|
|
|
|
The improvements will focus on three points:
|
|
|
|
- To allow multiple Bot instances to be associated with a single Bot when using
|
|
the `token` join method.
|
|
- To allow multiple Bot instances to be joined using a single join token when
|
|
using the `token` join method.
|
|
- Providing a way to track and monitor Bot instances.
|
|
|
|
## Why
|
|
|
|
Whilst deploying a large fleet of Bots is fairly trivial when using the
|
|
delegated and TPM join methods, the experience when managing a fleet of bot
|
|
hosts on-prem is more challenging.
|
|
|
|
The following burdens currently exist:
|
|
|
|
- When using the `token` join method, a Bot must be created for each Bot instance. This
|
|
means that the privileges of many distinct Bots need to be synchronized where
|
|
those hosts are performing the same function.
|
|
- When using the `token` join method, a token can only be used once. This means
|
|
creating hundreds of join tokens and managing securely distributing these to
|
|
hosts.
|
|
- When managing a large fleet of Bot instances, there is no way to
|
|
track these within Teleport. This makes it more difficult to identify hosts
|
|
which may need updating.
|
|
|
|
As we look to onboard more Enterprise customers to Machine ID, the pains of
|
|
the on-prem experience have become more apparent. Enterprise customers are
|
|
more likely to have on-prem deployments and these are likely to be larger in
|
|
scale.
|
|
|
|
## Details
|
|
|
|
### Current State
|
|
|
|
Currently, the `token` join method introduces a generation counter as a label
|
|
on the Bot user. This counter is contained within the Bot certificate and on
|
|
each renewal, this counter is incremented. When the counter within the certificate
|
|
de-synchronizes with the counter on the user, the Bot is locked out as a security
|
|
measure.
|
|
|
|
The fact that this counter is stored within a label on the Bot user creates a
|
|
one-to-one binding between a single instance of `tbot` and a single Bot user.
|
|
This is not the case when using the delegated join methods.
|
|
|
|
### Persistent Bot Instance Identity
|
|
|
|
Today, there is no persistent identifier for an individual instance of a Bot.
|
|
Instead, all `tbot` instances are effectively identified solely by their Bot
|
|
identity. There is no easy way to distinguish them. On each renewal, `tbot`
|
|
regenerates the private-public key pair that is used within its certificate and
|
|
there is no other form of unique ID that is persisted across renewals.
|
|
|
|
This poses a few challenges:
|
|
|
|
- For the purposes of auditing, it is not possible to trace actions to a
|
|
specific instance of a Bot.
|
|
- For improving the `token` join method to support multiple Bot instances
|
|
associated with a single Bot, there is no identifier to correlate with the
|
|
generation counter.
|
|
- For analytics purposes, it's difficult for us to track the number of
|
|
individual Bot instances in use. We cannot easily determine if it's a single
|
|
very active Bot instance, or many less active Bot instances.
|
|
|
|
To rectify this, a unique identifier should be established for an instance of
|
|
a Bot.
|
|
|
|
#### Bot Identity Trust
|
|
|
|
We should be mindful that adding a new persistent identifier may increase our
|
|
attack surface, particularly if we allow clients manipulate their persistent
|
|
identifier or in any way trust the values communicated to Auth during the join
|
|
process.
|
|
|
|
For example, trusting this identifier might allow a bot to better masquerade
|
|
itself as a preexisting instance and avoid discovery by an end user that falsely
|
|
assumed no unexpected instances had joined their cluster. Additionally, if we
|
|
implement join limits (i.e. tokens that only allow N bots to join), malicious
|
|
bots could reuse existing identifiers to bypass join limits.
|
|
|
|
To mitigate this, we should make certain to cryptographically verify identifiers
|
|
during the renewal process. For example, we can embed the identifier as a
|
|
certificate field to ensure it cannot be tampered with once issued by the Auth
|
|
service, or encrypt the renewed certificates using the previous iteration's
|
|
public key to ensure the calling bot owns the private key. Adopting proper mTLS
|
|
during the join process should accomplish both of these goals.
|
|
|
|
##### Verifying Bot Identities
|
|
|
|
We currently see two methods for cryptographically verifying bot identities at
|
|
renewal time:
|
|
|
|
1. We could expose the existing functionality of the HTTPS-only
|
|
`RegisterUsingToken` over gRPC. The existing gRPC `JoinService` can be
|
|
accessed with and without authentication, so we could inspect the client
|
|
connection to find the existing bot identity, if any, and it would be
|
|
implicitly verified.
|
|
2. We could adapt the existing HTTPS implementation of `RegisterUsingToken` to
|
|
additionally accept an encoded existing certificate, and return certificates
|
|
encrypted with the certificate's public key. We can verify the certificate
|
|
was originally signed with our CA, and the client will only be able to
|
|
decrypt the returned identity if they actually have the private key for the
|
|
previous identity.
|
|
3. Accept client certificate authentication in HTTPS join methods. gRPC join
|
|
methods already accept client certificates, however these are authenticated
|
|
in the Join Service rather than in Auth directly. As such, gRPC join methods
|
|
will need to pass along the bot instance ID in a trusted field.
|
|
|
|
Our preference is option (3): bots and other clients join as usual, but will
|
|
always provide a client certificate when one is available. Auth will use this
|
|
client certificate to pair joining bots with their previous instances.
|
|
|
|
Additionally, a downside to certificate verification is that the certificate
|
|
validity period becomes a factor. If bots are only run intermittently, like
|
|
from a CI workflow, their certificates could expire and prevent them from
|
|
being identified as the same instance. This is likely to only impact a small
|
|
number of cases, however, as most CI provider joins are stateless and have no
|
|
certificates to present anyway. Bots that present expired certificates will
|
|
either be rejected and will need to join as a new instance.
|
|
|
|
#### UUID Certificate Attribute
|
|
|
|
On the initial join of a Bot instance, we could generate a UUID to identify that
|
|
Bot instance and encode this within the certificate. Upon renewals, the UUID
|
|
would be copied from the current certificate and into the new one.
|
|
|
|
This method additionally gives us freedom to change various join parameters
|
|
while preserving the lineage of a bot identity. Bots could change their keypair
|
|
or join method and still be properly associated with their previous iteration.
|
|
|
|
Whilst this is fairly easy to implement for the `token` join method, one
|
|
challenge for the delegated join methods is that rather than renewing, the
|
|
`tbot` instance merely re-joins. As the join RPCs are unauthenticated, the
|
|
previous certificate of the Bot instance is not readily available. We can
|
|
either:
|
|
|
|
- Accept this limitation and treat each renewal of a delegated Bot instance as a
|
|
new Bot instance. This is likely unacceptable and would limit any advantages
|
|
of this work to the `token` join method.
|
|
- Add support for calling the join RPCs with a client certificate.
|
|
|
|
Given our desire to ensure this identifier is trustworthy, we should prefer to
|
|
support the latter case and verify client certificates at re-joining time.
|
|
|
|
#### Alternative: Public Key Fingerprint
|
|
|
|
One option is to modify the behaviour of `tbot` to persist and reuse the
|
|
keypair across renewals. We could then use a fingerprint of the public key as a
|
|
unique identifier of the Bot instance.
|
|
|
|
This feels like a natural identifier. It avoids introducing a new attribute to
|
|
certificates as the public key is already encoded within certificates. The
|
|
nature of public-key cryptography also means that this provides the Bot instance
|
|
a way to identify itself without needing an issued certificate.
|
|
|
|
To mitigate the risk of pre-image attacks, SHA256 will be used to determine the
|
|
fingerprint of the public key. In addition, the full public key should be
|
|
recorded and verified against when authenticating a Bot instance action.
|
|
|
|
It should be noted that with this technique, rotating the keypair of a `tbot`
|
|
instance would reset the identity of that instance. Rotation of this keypair
|
|
would be unusual and this side effect seems expected.
|
|
|
|
This technique does have some downsides:
|
|
|
|
- It makes it impossible to rotate a bot's private key. However, we do not
|
|
currently support this today, and purging a bot's data directory to do so
|
|
would simply result in a new `BotInstance`, which is likely an acceptable
|
|
workaround.
|
|
- Our join process today is unable to cryptographically verify the public key
|
|
presented by a joining bot to ensure that particular keypair has been issued
|
|
an identity already. Clients can provide any public key they like, including
|
|
that of an existing bot.
|
|
|
|
Given these downsides, we'll prefer to implement UUID instance identifiers.
|
|
Most of the technical challenge lies in adapting the join process to accept
|
|
client certificate authentication for re-joins, at which point adding a new
|
|
certificate field is trivial.
|
|
|
|
### BotInstance Resource
|
|
|
|
The
|
|
[Resource Guidelines RFD](https://github.com/gravitational/teleport/blob/master/rfd/0153-resource-guidelines.md)
|
|
will be followed.
|
|
|
|
With a persistent identifier for a Bot instance established, we can now track
|
|
information about a specific Bot instance server-side. In addition to providing
|
|
a way to store a generation counter per instance, this yields other benefits:
|
|
|
|
- Allows Bot instances to be viewed within the UI and CLI.
|
|
- Allowing Bot instances to submit basic self-reported information about itself
|
|
and its host, e.g:
|
|
- `tbot` version
|
|
- Hostname, OS and OS version
|
|
- The configuration of `tbot`
|
|
- Health status
|
|
- Record metadata from delegated joins to enrich information about the Bot.
|
|
E.g show the linked repository / CI run number
|
|
- Billing based on Bot instances rather than Bots.
|
|
|
|
Some of this information is known and verified by the server - for example, the
|
|
certificate generation or the join metadata. Some of this information is
|
|
self-reported and should not be trusted. The information from these two sources
|
|
should be segregated to avoid confusion.
|
|
|
|
BotInstance will be a new resource type introduced to track this information.
|
|
|
|
```protobuf
|
|
syntax = "proto3";
|
|
|
|
import "teleport/header/v1/metadata.proto";
|
|
|
|
// A BotInstance
|
|
message BotInstance {
|
|
// The kind of resource represented.
|
|
string kind = 1;
|
|
// Differentiates variations of the same kind. All resources should
|
|
// contain one, even if it is never populated.
|
|
string sub_kind = 2;
|
|
// The version of the resource being represented.
|
|
string version = 3;
|
|
// Common metadata that all resources share.
|
|
teleport.header.v1.Metadata metadata = 4;
|
|
// The configured properties of a BotInstance.
|
|
BotInstanceSpec spec = 5;
|
|
// Fields that are set by the server as results of operations. These should
|
|
// not be modified by users.
|
|
BotInstanceStatus status = 6;
|
|
}
|
|
|
|
message BotInstanceSpec {
|
|
// Empty as is not user configurable.
|
|
// Eventually this could be leveraged for simple command and control?
|
|
}
|
|
|
|
// BotInstanceStatusHeartbeat contains information self-reported by an instance
|
|
// of a Bot. This information is not verified by the server and should not be
|
|
// trusted.
|
|
message BotInstanceStatusHeartbeat {
|
|
// The timestamp that the heartbeat was recorded by the Auth Server. Any
|
|
// value submitted by `tbot` for this field will be ignored.
|
|
google.protobuf.Timestamp recorded_at = 1;
|
|
// Indicates whether this is the heartbeat submitted by `tbot` on startup.
|
|
bool is_startup = 2;
|
|
// The version of `tbot` that submitted this heartbeat.
|
|
string version = 3;
|
|
// The hostname of the host that `tbot` is running on.
|
|
string hostname = 4;
|
|
// The duration that `tbot` has been running for when it submitted this
|
|
// heartbeat.
|
|
google.protobuf.Duration uptime = 5;
|
|
// The currently configured join_method.
|
|
string join_method = 6;
|
|
// Indicates whether `tbot` is running in one-shot mode.
|
|
bool one_shot = 7;
|
|
// In future iterations, additional information can be submitted here.
|
|
// For example, the configuration of `tbot` or the health of individual
|
|
// outputs.
|
|
}
|
|
|
|
// BotInstanceStatusAuthentication contains information about a join or renewal.
|
|
// Ths information is entirely sourced by the Auth Server and can be trusted.
|
|
message BotInstanceStatusAuthentication {
|
|
// The timestamp that the join or renewal was authenticated by the Auth
|
|
// Server.
|
|
google.protobuf.Timestamp authenticated_at = 1;
|
|
// The join method used for this join or renewal.
|
|
string join_method = 2;
|
|
// The join token used for this join or renewal. This is only populated for
|
|
// delegated join methods as the value for `token` join methods is sensitive.
|
|
string join_token = 3;
|
|
// The metadata sourced from the join method.
|
|
google.protobuf.Struct metadata = 4;
|
|
// On each renewal, this generation is incremented. For delegated join
|
|
// methods, this counter is not checked during renewal. For the `token` join
|
|
// method, this counter is checked during renewal and the Bot is locked out if
|
|
// the counter in the certificate does not match the counter of the last
|
|
// authentication.
|
|
int32 generation = 5;
|
|
// The public key of the Bot instance.
|
|
// When authenticating a Bot instance, the full public key must be compared
|
|
// rather than just the fingerprint to mitigate pre-image attacks.
|
|
bytes public_key = 6;
|
|
// The fingerprint of the public key of the Bot instance.
|
|
string fingerprint = 7;
|
|
}
|
|
|
|
// BotInstanceStatus holds the status of a BotInstance.
|
|
message BotInstanceStatus {
|
|
// The unique identifier for this bot.
|
|
string id = 1;
|
|
// The name of the Bot that this instance is associated with.
|
|
string bot_name = 2;
|
|
// The initial authentication status for this bot instance.
|
|
BotInstanceStatusAuthentication initial_authentication = 3;
|
|
// The N most recent authentication status records for this bot instance.
|
|
repeated BotInstanceStatusAuthentication latest_authentications = 4;
|
|
// The initial heartbeat status for this bot instance.
|
|
BotInstanceStatusHeartbeat initial_heartbeat = 5;
|
|
// The N most recent heartbeats for this bot instance.
|
|
repeated BotInstanceStatusHeartbeat latest_heartbeats = 6;
|
|
}
|
|
```
|
|
|
|
The name used for a BotInstance will be a concatenation of the Bot name and its
|
|
unique identifier (UUID).
|
|
|
|
When storing the BotInstance in the backend, the key will be:
|
|
`bot_instances/{bot_name}/{uuid}`. This will allow for efficient listing
|
|
of BotInstances for a given Bot.
|
|
|
|
Like agent heartbeats, the BotInstance will expire after a period of inactivity.
|
|
This avoids the accumulation of ephemeral BotInstances.
|
|
|
|
#### Recording Authentication Data
|
|
|
|
Upon each join and renewal, the BotInstance record will be updated with an
|
|
additional entry in the `status.authentications` field. If there is X entries,
|
|
then the oldest entry will be removed. This prevents growth without bounds but
|
|
also ensures that the original record is retained.
|
|
|
|
In addition, the TTL of the BotInstance resource will be extended to cover the
|
|
validity period of the issued certificate, plus a short additional time to allow
|
|
for some imprecision.
|
|
|
|
If a BotInstance does not exist, then one will be created. In the case that
|
|
this occurs for a bot using the `token` join method and this is a renewal,
|
|
a warning will be emitted and the initial generation of the BotInstance will
|
|
be sourced from the certificates current generation counter. This behaviour
|
|
will support the migration of existing `tbot` instances to the new BotInstance
|
|
behaviour.
|
|
|
|
#### Recording Heartbeat Data
|
|
|
|
A new RPC will be added for submitting heartbeat data:
|
|
|
|
```protobuf
|
|
syntax = "proto3";
|
|
|
|
package teleport.machineid.v1;
|
|
|
|
service BotInstanceService {
|
|
// SubmitHeartbeat submits a heartbeat for a BotInstance.
|
|
rpc SubmitHeartbeat(SubmitHeartbeatRequest) returns (SubmitHeartbeatResponse);
|
|
}
|
|
|
|
// The request for SubmitHeartbeat.
|
|
message SubmitHeartbeatRequest {
|
|
// The heartbeat data to submit.
|
|
BotInstanceStatusHeartbeat heartbeat = 1;
|
|
}
|
|
|
|
// The response for SubmitHeartbeat.
|
|
message SubmitHeartbeatResponse {
|
|
// Empty
|
|
}
|
|
```
|
|
|
|
The endpoint will have a special auth/authz check. RBAC will not be used and
|
|
instead the endpoint will ensure that the presented client certificate is for
|
|
the Bot linked to the instance.
|
|
|
|
This endpoint will be called by `tbot` immediately after it has initially
|
|
authenticated. After a heartbeat has successfully completed, another should be
|
|
scheduled for a half hour after. A small amount of jitter should be added to
|
|
the heartbeat period to avoid a thundering herd of heartbeats.
|
|
|
|
If the heartbeat fails, then `tbot` should retry on an exponential backoff.
|
|
|
|
##### Alternative: Submit Heartbeat Data on Join/Renew
|
|
|
|
Alternatively, we could add a Heartbeat field to the join/renew RPCs.
|
|
|
|
Pros:
|
|
|
|
- Avoids introducing a new RPC and ensures that all data within the Heartbeat
|
|
comes from the same instance in time.
|
|
- Allows self-reported information to be used as part of renewal decision.
|
|
This is not a strong defense as it is self-reported and cannot be trusted.
|
|
- Avoids a state where the BotInstance is incomplete immediately after joining
|
|
and before it has called SubmitHeartbeat.
|
|
|
|
Cons:
|
|
|
|
- Adds Bot specific behaviour to RPCs that are also used for Node joining.
|
|
- Heartbeats are limited to the interval of renewal.
|
|
|
|
Given these cons, we'll opt introduce the new heartbeat RPC.
|
|
|
|
#### API
|
|
|
|
Additional RPCs will be added to the BotInstance service to allow these to
|
|
be listed and deleted:
|
|
|
|
```protobuf
|
|
syntax = "proto3";
|
|
|
|
package teleport.machineid.v1;
|
|
|
|
service BotInstanceService {
|
|
// GetBotInstance returns the specified BotInstance resource.
|
|
rpc GetBotInstance(GetBotInstanceRequest) returns (BotInstance);
|
|
// ListBotInstances returns a page of BotInstance resources.
|
|
rpc ListBotInstances(ListBotInstancesRequest) returns (ListBotInstancesResponse);
|
|
// DeleteBotInstance hard deletes the specified BotInstance resource.
|
|
rpc DeleteBotInstance(DeleteBotInstanceRequest) returns (google.protobuf.Empty);
|
|
}
|
|
|
|
// Request for GetBotInstance.
|
|
message GetBotInstanceRequest {
|
|
// The name of the bot associated with the instance.
|
|
string bot_name = 1;
|
|
// The unique identifier of the bot instance to retrieve.
|
|
string id = 2;
|
|
}
|
|
|
|
// Request for ListBotInstances.
|
|
//
|
|
// Follows the pagination semantics of
|
|
// https://cloud.google.com/apis/design/standard_methods#list
|
|
message ListBotInstancesRequest {
|
|
// The name of the Bot to list BotInstances for. If empty, all BotInstances
|
|
// will be listed.
|
|
string filter_bot_name = 1;
|
|
// The maximum number of items to return.
|
|
// The server may impose a different page size at its discretion.
|
|
int32 page_size = 2;
|
|
// The page_token value returned from a previous ListBotInstances request, if
|
|
// any.
|
|
string page_token = 3;
|
|
}
|
|
|
|
// Response for ListBotInstances.
|
|
message ListBotInstancesResponse {
|
|
// BotInstance that matched the search.
|
|
repeated BotInstance bot_instances = 1;
|
|
// Token to retrieve the next page of results, or empty if there are no
|
|
// more results exist.
|
|
string next_page_token = 2;
|
|
}
|
|
|
|
// Request for DeleteBotInstance.
|
|
message DeleteBotInstanceRequest {
|
|
// The name of the BotInstance to delete.
|
|
string bot_name = 1;
|
|
// The unique identifier of the bot instance to delete.
|
|
string id = 2;
|
|
}
|
|
```
|
|
|
|
### Changes to the `token` Join Method
|
|
|
|
As we now have a way to track the generation for a specific Bot instance, we
|
|
can allow multiple Bot instances to be associated with a single Bot. This
|
|
also means that the token no longer needs to be consumed on a join.
|
|
|
|
However, this does introduce a change in our security guarantees, and without
|
|
additional tooling support and sensible defaults, the change may incentivize end
|
|
users to create long-lived join tokens instead of using a more appropriate join
|
|
method, or automating issuance of short lived tokens.
|
|
|
|
To this end, we should introduce a per-bot-instance join count limit, and
|
|
configure that to be 1 join by default. This matches today's behavior, and
|
|
will help ensure users do not accidentally create a token that provides more
|
|
access than expected: "infinite use" tokens with massive join limits and/or very
|
|
long TTLs will need to be explicitly specified.
|
|
|
|
We may additionally want to put hurdles in the way of things like extremely long
|
|
token TTLs. There are legitimate low-security use cases for these, but we could
|
|
introduce a soft limit in `tctl` preventing automatic token creation with TTL
|
|
longer than 7 days, forcing users to manually create the token if they really
|
|
need it to last longer.
|
|
|
|
Additionally, renewal logic will need to be adjusted to read and update the
|
|
generation counter from the BotInstance rather than the Bot user. We should also
|
|
take care to ensure this counter behaves sensibly even when many bots are
|
|
attempting to join the cluster concurrently. The generation counter today
|
|
already has issues with concurrent joins, and it's even more important to get
|
|
this right when we can expect contention over a single bot instance resource.
|
|
|
|
### CLI Changes
|
|
|
|
#### `tbot`
|
|
|
|
`tbot reset` will be added to allow a `tbot` instance to be reset. This will
|
|
simply clear out any artifacts within the `tbot` storage directory.
|
|
|
|
In addition, if the bot detects a change in join token or join method, it should
|
|
automatically rotate its keypair. This will ensure it presents as a fresh
|
|
BotInstance to the AuthServer.
|
|
|
|
A log message should be output that identifies the linked BotInstance at
|
|
startup and on each heartbeat. This will allow users to easily correlate the
|
|
`tbot` installation with a BotInstance.
|
|
|
|
#### `tctl`
|
|
|
|
Commands to list all BotInstances and the BotInstances for a specific Bot should
|
|
be added to the `tctl bots` family:
|
|
|
|
`tctl bots instances list`
|
|
`tctl bots instances list --bot <bot name>`
|
|
|
|
The `tctl tokens add` command should be extended to allow a new token to be
|
|
associated with an existing Bot now that multiple Bot instances can be run
|
|
against a single Bot:
|
|
|
|
`tctl tokens add --type=bot --bot <bot name>`
|
|
|
|
Additionally, `tctl rm`/`tctl get` should be able to operate on BotInstance.
|
|
|
|
There is no requirement for it to be possible to create or update a BotInstance
|
|
with `tctl`.
|
|
|
|
### Analytics
|
|
|
|
A PostHog event should be emitted for each BotInstance heartbeat. This will
|
|
allow us to track active bots in a similar way to how we track active agents.
|
|
|
|
```protobuf
|
|
// a heartbeat for a Bot Instance
|
|
//
|
|
// PostHog event: tp.bot.instance.hb
|
|
message BotInstanceHeartbeatEvent {
|
|
// anonymized name of the instance, 32 bytes (HMAC-SHA-256);
|
|
bytes bot_instance_name = 1;
|
|
// the version of tbot
|
|
string version = 2;
|
|
// indicates whether or not tbot is running in one-shot mode
|
|
bool one_shot = 3;
|
|
// indicates the configured join method of `tbot`.
|
|
string join_method = 4;
|
|
}
|
|
```
|
|
|
|
Existing analytics for join, renewal and certificate generation should be
|
|
extended to include the BotInstance ID anonymized. This will allow them to be
|
|
linked together.
|
|
|
|
### Migration/Compatibility
|
|
|
|
The "create if not exists" behaviour of the BotInstance resource will mean that
|
|
existing Bot instances will have a BotInstance resource created on their first
|
|
renewal after this feature is released. Their existing generation counter will
|
|
be trusted on this first renewal. This allows for a seamless migration to the
|
|
new system.
|
|
|
|
Older `tbot` instances will not submit heartbeats. This means that their
|
|
BotInstance will only contain authentication data. Any CLI or GUI that shows
|
|
BotInstances should show a gracefully degraded state in this case that explains
|
|
that the `tbot` needs to be upgraded.
|
|
|
|
## Security Considerations
|
|
|
|
### Audit Events
|
|
|
|
An audit event should be added for the deletion of a BotInstance. The
|
|
name of the BotInstance should be added to the existing join, renewal and
|
|
certificate generation audit events.
|
|
|
|
Additionally, we should ensure bot instance identifiers are present in existing
|
|
audit events to ensure actions taken by bots can be traced back to specific
|
|
instances.
|
|
|
|
### Resistance to collision/pre-image attacks
|
|
|
|
We should be cautious that a BotInstance cannot be impersonated using a
|
|
second pre-image attack. This risk is introduced by using the public key
|
|
fingerprint as an identifier.
|
|
|
|
To mitigate this, we should ensure that the full public key is compared
|
|
to the recorded one when authenticating a BotInstance rather than merely
|
|
comparing the fingerprint.
|
|
|
|
In addition, a more modern hashing algorithm should be used to calculate the
|
|
fingerprint. In this case, we have selected SHA256 as this is more resistant
|
|
compared to hash functions such as MD5 or SHA1.
|
|
|
|
## Alternatives
|
|
|
|
### Defer the Heartbeats work and solely improve the `token` join method
|
|
|
|
We could modify the way we record the generation counter for the `token` join
|
|
method without introducing the BotInstance resource. Instead of storing a
|
|
single counter within the Bot User labels, we could store a JSON encoded map
|
|
of counters.
|
|
|
|
One challenge would be contention over the user resource if a large number of
|
|
Bot instances are trying to renew their certificates at the same time. Our
|
|
Backend has limited support for transactional consistency and this increases the
|
|
risk of two Bot instances renewing simultaneously and producing an inconsistent
|
|
state that locks one of them out.
|
|
|
|
### Introduce renewal-less and generation-less `token` join method
|
|
|
|
One option is to introduce a new join method that does not produce renewable
|
|
certificates. There would be no need for the fragile generation counter
|
|
and the join token would be continually re-used to join as is done for the
|
|
delegated join methods. This also circumvents the need for a one-to-one binding
|
|
between a Bot instance and a Bot.
|
|
|
|
This token would be incredibly sensitive and if stolen, there would be no
|
|
automated mechanisms to detect this as exists today with the generation counter.
|
|
|
|
It likely makes more sense to improve the existing `token` join method rather
|
|
than introduce a variant which behaves differently and is less secure. It would
|
|
increase the complexity of the codebase and the user experience.
|
|
|
|
## Out of Scope
|
|
|
|
These tasks are out of scope of this RFD but could be considered natural
|
|
follow-on tasks.
|
|
|
|
### Multi-phase Commit of Generation Counter
|
|
|
|
Currently, the generation counter is fragile as it is incremented server side
|
|
without confirmation that `tbot` has been able to use and persist the new
|
|
credentials. If `tbot` does not receive confirmation of the renewal or is
|
|
unable to persist the new credentials, it will be locked out on it's next
|
|
attempt to renew.
|
|
|
|
We could introduce a multi-phase commit of the generation counter. This would
|
|
provide more robustness to the renewal process.
|
|
|
|
### Locking of Individual Bot Instances
|
|
|
|
Currently, it's only possible to lock out an entire Bot user. This means that
|
|
when managing a large fleet, it would not be able to lock out a specific host
|
|
that had been compromised. This is likely to be a major friction point for those
|
|
deploying a large number of Bot instances.
|
|
|
|
It also increases the significance of the fragility of the generation counter.
|
|
|
|
### Bot Command and Control
|
|
|
|
The BotInstance resource could be extended to allow `tbot` to be controlled
|
|
remotely.
|