Documentation
¶
Index ¶
- Constants
- Variables
- func BackfillMetadata(now time.Time) ([]string, error)
- func ClassifyAPIError(operation string, err error) error
- func ClientSecretAuthKey(clientSecret string, opts AuthKeyOptions) (string, error)
- func DeleteAPIKey()
- func DeleteAPIKeyChecked() error
- func DeleteClientSecret()
- func DeleteClientSecretChecked() error
- func DeleteSlotMetadata(slot string) error
- func DeleteSlotMetadataLocked(slot string) error
- func DeleteStoredCredentialKindStrict(slot string) error
- func DeleteStoredCredentialKindStrictLocked(slot string) error
- func DeleteStoredCredentialsStrict() error
- func DeleteStoredCredentialsStrictLocked() error
- func DeriveAuthKey(ctx context.Context, opts AuthKeyOptions) (string, error)
- func ExpiryState(meta *SlotMetadata, present bool, now time.Time) (string, *int)
- func Fingerprint(value string) string
- func GetAPIKey() (string, error)
- func GetAuthKey(ctx context.Context, opts AuthKeyOptions) (string, error)
- func GetClientSecret() (string, error)
- func HasClientSecret() bool
- func HasStoredCredential() (bool, error)
- func IsolateForTesting()
- func MigrateFromLegacy() (migrated bool)
- func NewTailscaleClient() (*tailscale.Client, error)
- func NewTailscaleClientWithAPIKey(key string) (*tailscale.Client, error)
- func NewTailscaleClientWithUserOwnedAPIKey() (*tailscale.Client, error)
- func NextAPIForbidden() []string
- func NextAPIKeyBootstrap() []string
- func NextOAuthBootstrap() []string
- func RecordCredentialStored(slot, value string, opts StoredOptions) (SlotMetadata, *SlotMetadata, error)
- func RecordVerification(slot, fingerprint, result string, now time.Time) error
- func RemoveMetadataFile() error
- func RemoveMetadataFileLocked() error
- func RequireStoredCredential() error
- func SaveClientSecret(secret string) error
- func SaveMetadata(doc Metadata) error
- func SetAPIKey(key string) error
- func SetMutationLockPathForTesting(path string) (restore func())
- func SetMutationLockTimeoutForTesting(timeout time.Duration) (restore func())
- func SlotKind(slot string) string
- func ValidSlot(slot string) bool
- func VerificationResultForError(err error) string
- func WithMutationTransaction(fn func(*MutationTransaction) error) error
- func WorstExpiryState(states ...string) string
- func WriteSlotMetadata(slot string, meta SlotMetadata) error
- func WriteSlotMetadataLocked(slot string, meta SlotMetadata) error
- type AuthKeyOptions
- type CredentialBackend
- type CredentialFileSnapshot
- type CredentialKindStatus
- type CredentialLocationStatus
- type Inventory
- type Metadata
- type MutationTransaction
- func (*MutationTransaction) DeleteAPIKeyChecked() error
- func (*MutationTransaction) DeleteClientSecretChecked() error
- func (*MutationTransaction) RestoreRemovedCredentialFiles(snapshot CredentialFileSnapshot) error
- func (*MutationTransaction) SaveClientSecretWithBackend(secret string) (CredentialBackend, error)
- func (*MutationTransaction) SetAPIKeyWithBackend(key string) (CredentialBackend, error)
- func (*MutationTransaction) SnapshotCredentialFiles() CredentialFileSnapshot
- type ProbeOutcome
- type SlotMetadata
- type SlotValues
- type SlotView
- type StoredCredentialStatus
- type StoredOptions
- type TailscaleClientFactory
Constants ¶
const ( KeysPageURL = "https://login.tailscale.com/admin/settings/keys" OAuthPageURL = "https://login.tailscale.com/admin/settings/oauth" )
Admin-console pages an operator must visit by hand. TSLink cannot mint a user-owned access token or an OAuth client for the operator, so every credential-related error and expiry warning points here instead of only saying "run tslink login".
const ( MetadataSchemaVersion = 1 SlotAPIKey = "api-key" SlotClientSecret = "client-secret" KindAPIAccessToken = "api_access_token" KindOAuthClientSecret = "oauth_client_secret" ExpirySourceUser = "user" ExpirySourceAssumedMax = "assumed_max" VerifyResultOK = "ok" VerifyResultForbidden = "forbidden" VerifyResultUnreachable = "unreachable" ExpiryStateNone = "none" ExpiryStateOK = "ok" ExpiryStateExpiring = "expiring" ExpiryStateExpired = "expired" ExpiryStateUnknown = "unknown" // APIKeyAssumedMaxLifetime is Tailscale's documented upper bound for a // user-owned access token. When the operator does not state the real expiry // at login time, TSLink assumes this maximum and records the assumption as // expires_at_source=assumed_max so consumers can tell it apart from fact. APIKeyAssumedMaxLifetime = 90 * 24 * time.Hour // ExpiringSoonThreshold is the window before expires_at in which status and // doctor report the api-key slot as expiring rather than ok. ExpiringSoonThreshold = 14 * 24 * time.Hour )
Value-free credential metadata. The file records what TSLink knows about each stored credential without ever holding the credential itself: a short sha256 fingerprint, when it was stored, when it is expected to expire, and the last remote verification result. A missing or corrupt file never blocks a command; readers report the affected slot as unknown and doctor surfaces a warning.
Variables ¶
var ErrCredentialWritePartial = errors.New("credential write may be partially committed")
ErrCredentialWritePartial means compensation after a failed write could not prove the prior keyring value was restored. Callers with a wider transaction should attempt their own snapshot rollback and report the partial state.
var ErrMetadataCorrupt = errors.New("credential metadata file is unreadable or malformed")
ErrMetadataCorrupt reports that credential-meta.json exists but cannot be trusted. Callers treat the affected slots as unknown and continue.
var ErrMutationLockBusy = errors.New("credential transaction lock busy")
ErrMutationLockBusy reports that another credential transaction held the lock for the whole wait. It is wrapped in a registry.StableCodeError with the CLI's retryable "conflict" code.
var ErrUnknownCredentialSlot = errors.New("unknown credential slot")
ErrUnknownCredentialSlot rejects slot names outside the two supported slots.
var ErrUserOwnedAPIKeyRequired = errors.New("user-owned Tailscale API access token required")
ErrUserOwnedAPIKeyRequired means an operation cannot use a tailnet-owned OAuth client and needs an API access token tied to an inviting user.
Functions ¶
func BackfillMetadata ¶
BackfillMetadata records metadata for any stored credential that lacks it. It is the daemon-startup migration counterpart of MigrateFromLegacy and returns the slots it backfilled.
func ClassifyAPIError ¶
ClassifyAPIError turns a Tailscale REST failure into a stable, actionable error. HTTP 401 means the credential itself is expired, revoked, or invalid and maps to api_token_unauthorized with the key-bootstrap steps; HTTP 403 means the credential is valid but lacks the role or scope and maps to api_forbidden. Every other failure is returned wrapped with the operation label and no stable code, exactly as before. The wrapped chain is preserved so callers that classify with errors.Is keep working.
func ClientSecretAuthKey ¶
func ClientSecretAuthKey(clientSecret string, opts AuthKeyOptions) (string, error)
ClientSecretAuthKey builds a tsnet auth key from a candidate OAuth client secret without reading or mutating any persisted credential. It is used to semantically validate a client secret via a disposable, ephemeral Up before the login transaction commits and retires the previous credential.
func DeleteAPIKey ¶
func DeleteAPIKey()
DeleteAPIKey removes the API key from all storage locations.
func DeleteAPIKeyChecked ¶
func DeleteAPIKeyChecked() error
DeleteAPIKeyChecked removes the API key from all storage locations and surfaces cleanup failures for callers that need transactional semantics.
func DeleteClientSecret ¶
func DeleteClientSecret()
DeleteClientSecret removes the client secret from all storage locations.
func DeleteClientSecretChecked ¶
func DeleteClientSecretChecked() error
DeleteClientSecretChecked removes the OAuth client secret from all storage locations and surfaces cleanup failures for transactional credential swaps.
func DeleteSlotMetadata ¶
DeleteSlotMetadata removes one slot record under the credential mutation lock. When the file is corrupt or no slots remain, the whole file is removed.
func DeleteSlotMetadataLocked ¶
DeleteSlotMetadataLocked is DeleteSlotMetadata for a caller that already holds the credential mutation lock inside WithMutationTransaction.
func DeleteStoredCredentialKindStrict ¶
DeleteStoredCredentialKindStrict removes exactly one credential slot from every supported store and reads back each store, mirroring DeleteStoredCredentialsStrict for selective logout.
func DeleteStoredCredentialKindStrictLocked ¶
DeleteStoredCredentialKindStrictLocked is DeleteStoredCredentialKindStrict for a caller that already holds the credential mutation lock, such as logout removing a slot and its metadata in one transaction.
func DeleteStoredCredentialsStrict ¶
func DeleteStoredCredentialsStrict() error
DeleteStoredCredentialsStrict removes API-key/client-secret credentials from every supported store and reads back each store. It keeps going after errors and returns their join so callers never report success while residual credential risk remains.
func DeleteStoredCredentialsStrictLocked ¶
func DeleteStoredCredentialsStrictLocked() error
DeleteStoredCredentialsStrictLocked is DeleteStoredCredentialsStrict for a caller that already holds the credential mutation lock, such as logout removing values and metadata in one transaction.
func DeriveAuthKey ¶
func DeriveAuthKey(ctx context.Context, opts AuthKeyOptions) (string, error)
DeriveAuthKey creates a short-lived, single-use, pre-authorized auth key from the API key.
func ExpiryState ¶
ExpiryState classifies one slot. A present slot without trusted metadata is unknown; an absent slot is none; a slot with no expires_at is ok.
func Fingerprint ¶
Fingerprint returns the first eight hex characters of sha256(value). It is safe to print: it identifies a credential without revealing it.
func GetAPIKey ¶
GetAPIKey retrieves the API key from keychain or file. Returns ("", nil) if no key is stored.
func GetAuthKey ¶
func GetAuthKey(ctx context.Context, opts AuthKeyOptions) (string, error)
GetAuthKey returns a usable auth key, trying (in order):
- Client secret (OAuth long-lived credential, used directly)
- Derive from API key (if available)
- Read legacy authkey file
func GetClientSecret ¶
GetClientSecret retrieves the client secret from keychain or file. Returns ("", nil) if no client secret is stored.
func HasClientSecret ¶
func HasClientSecret() bool
HasClientSecret returns true if a client secret is stored.
func HasStoredCredential ¶
HasStoredCredential reports whether any supported auth credential is configured without deriving or consuming a fresh auth key.
func IsolateForTesting ¶
func IsolateForTesting()
IsolateForTesting switches this process to go-keyring's in-memory mock, so a test can never read, write, or delete the operator's real OS keyring items. go-keyring offers no way back to the real provider once the mock is installed. It also moves the account credential lock into the config directory, which tests point at temporary directories, and refuses any lock path inside the real account home.
Every test binary that links go-keyring calls this from an init function in a _test.go file; that runs before TestMain and before any test, and self-exec children re-run the same binary and install it again. TestEveryKeyringLinkingTestBinaryInstallsIsolation enforces the rule. Production code must never call it.
func MigrateFromLegacy ¶
func MigrateFromLegacy() (migrated bool)
MigrateFromLegacy moves a file-based API key into the system keychain. Safe to call even if there's nothing to migrate.
func NewTailscaleClient ¶
NewTailscaleClient creates a Tailscale API client from stored credentials. OAuth client secrets take precedence over API keys, matching GetAuthKey. Returns (nil, nil) if no API credential is configured.
func NewTailscaleClientWithAPIKey ¶
NewTailscaleClientWithAPIKey creates a Tailscale API client from an explicit candidate key without reading or mutating persisted credentials.
func NewTailscaleClientWithUserOwnedAPIKey ¶
NewTailscaleClientWithUserOwnedAPIKey creates a Tailscale API client from the stored user-owned API access token only. It deliberately does not fall back to an OAuth client secret: invite mutations require an inviting user, while OAuth clients are tailnet-owned and have no user identity.
func NextAPIForbidden ¶
func NextAPIForbidden() []string
NextAPIForbidden is the recovery sequence for an HTTP 403: the credential is valid but its user role or OAuth scopes do not permit the operation.
func NextAPIKeyBootstrap ¶
func NextAPIKeyBootstrap() []string
NextAPIKeyBootstrap is the three-step recovery sequence for a missing, expired, or rejected user-owned tskey-api- token. Every error next list that used to say only "tslink login --api-key-stdin" references this one sequence.
func NextOAuthBootstrap ¶
func NextOAuthBootstrap() []string
NextOAuthBootstrap is the recovery sequence for adding a tailnet-owned OAuth client secret, the durable credential for daemon node authentication.
func RecordCredentialStored ¶
func RecordCredentialStored(slot, value string, opts StoredOptions) (SlotMetadata, *SlotMetadata, error)
RecordCredentialStored writes the slot record after a credential commit and returns the previous record when one existed. A corrupt metadata file is replaced: the new commit is the only fact TSLink can still vouch for. The read-modify-write runs under the credential mutation lock.
func RecordVerification ¶
RecordVerification stores the outcome of a remote probe of the credential with the given fingerprint. Under the credential mutation lock it records the verdict only if the slot's metadata still describes that credential: a slot without metadata, or one rotated while the probe ran, is left alone so a probe can never invent a stored_at or stamp a verdict on another value.
func RemoveMetadataFile ¶
func RemoveMetadataFile() error
RemoveMetadataFile deletes credential-meta.json under the credential mutation lock. Missing files are not errors.
func RemoveMetadataFileLocked ¶
func RemoveMetadataFileLocked() error
RemoveMetadataFileLocked is RemoveMetadataFile for a caller that already holds the credential mutation lock inside WithMutationTransaction.
func RequireStoredCredential ¶
func RequireStoredCredential() error
RequireStoredCredential returns a user-facing auth error if no credential exists.
func SaveClientSecret ¶
SaveClientSecret stores an OAuth client secret. It is the source-compatible wrapper for callers that do not need to report the selected backend.
func SaveMetadata ¶
SaveMetadata atomically writes credential-meta.json with owner-only permissions. An empty slot map removes the file instead.
func SetAPIKey ¶
SetAPIKey stores the API key. It is the source-compatible wrapper for callers that do not need to report the selected backend.
func SetMutationLockPathForTesting ¶
func SetMutationLockPathForTesting(path string) (restore func())
Used only by fake-keyring tests; production always resolves the OS account.
func SetMutationLockTimeoutForTesting ¶
Used only by lock-contention tests; production always waits 5 s.
func VerificationResultForError ¶
VerificationResultForError maps a remote probe outcome to the stored last_verified_result vocabulary.
func WithMutationTransaction ¶
func WithMutationTransaction(fn func(*MutationTransaction) error) error
func WorstExpiryState ¶
WorstExpiryState folds per-slot states into one headline state.
func WriteSlotMetadata ¶
func WriteSlotMetadata(slot string, meta SlotMetadata) error
WriteSlotMetadata replaces one slot record verbatim under the credential mutation lock.
func WriteSlotMetadataLocked ¶
func WriteSlotMetadataLocked(slot string, meta SlotMetadata) error
WriteSlotMetadataLocked is WriteSlotMetadata for a caller that already holds the credential mutation lock inside WithMutationTransaction. Login uses it to record a commit and to restore the previous record on rollback.
Types ¶
type AuthKeyOptions ¶
type AuthKeyOptions struct {
Tags []string
Ephemeral bool
Description string
ClientFactory TailscaleClientFactory
}
AuthKeyOptions configures the derived auth key.
type CredentialBackend ¶
type CredentialBackend string
CredentialBackend identifies the storage location that accepted a credential. It contains no credential material and is safe to report.
const ( CredentialBackendKeyring CredentialBackend = "keyring" CredentialBackendFile CredentialBackend = "file" )
func SaveClientSecretWithBackend ¶
func SaveClientSecretWithBackend(secret string) (CredentialBackend, error)
SaveClientSecretWithBackend stores an OAuth client secret and reports the backend that actually accepted it. The key must start with "tskey-client-". File fallback follows the same write-new, strictly-delete-old transaction as API keys.
func SetAPIKeyWithBackend ¶
func SetAPIKeyWithBackend(key string) (CredentialBackend, error)
SetAPIKeyWithBackend stores the API key and reports the backend that actually accepted it. A failed keyring write may fall back to a private file only after every file precondition is satisfied. It reports file success only after any stale keyring value has been removed and readback proves it absent.
type CredentialFileSnapshot ¶
type CredentialFileSnapshot struct {
// contains filtered or unexported fields
}
CredentialFileSnapshot holds the credential files that existed when a transaction began. It carries credential material: never log or persist it.
type CredentialKindStatus ¶
type CredentialKindStatus struct {
Keyring CredentialLocationStatus
File CredentialLocationStatus
}
CredentialKindStatus is a value-free presence report for all storage locations of one credential kind.
func (CredentialKindStatus) Present ¶
func (s CredentialKindStatus) Present() bool
type CredentialLocationStatus ¶
CredentialLocationStatus is a value-free presence report for one credential storage location. It deliberately never carries credential material.
type Inventory ¶
type Inventory struct {
APIKey SlotView
ClientSecret SlotView
// MetadataError is non-nil when credential-meta.json exists but is
// unreadable or malformed; present slots then report unknown.
MetadataError error
// Backfilled lists slots whose metadata was created or re-anchored during
// this call because a credential existed without matching bookkeeping.
Backfilled []string
// BackfillError is non-nil when a backfill could not be persisted.
BackfillError error
// ExpiryState is the worst per-slot state.
ExpiryState string
}
Inventory is the value-free report for both slots plus metadata health.
func DescribeSlots ¶
func DescribeSlots(values SlotValues, now time.Time, persist bool) Inventory
DescribeSlots computes the value-free inventory for the given stored values. When a slot holds a credential but has no matching metadata (or the stored fingerprint no longer matches), the slot is backfilled with stored_at=now and the assumed maximum lifetime; persist controls whether that backfill is written to disk, which happens only while the slot still stores that value. No disk access happens when neither slot is present.
func DescribeSlotsWithMetadata ¶
func DescribeSlotsWithMetadata(values SlotValues, doc Metadata, loadErr error, now time.Time) Inventory
DescribeSlotsWithMetadata is the pure classification step behind DescribeSlots: it never touches disk, so callers (and tests) can evaluate an inventory against an in-memory document. loadErr is the LoadMetadata error for doc; when non-nil every present slot is reported as unknown.
type Metadata ¶
type Metadata struct {
SchemaVersion int `json:"schema_version"`
Slots map[string]SlotMetadata `json:"slots"`
}
Metadata is the on-disk document.
func LoadMetadata ¶
LoadMetadata reads credential-meta.json. A missing file yields an empty document and no error. A malformed or unreadable file yields ErrMetadataCorrupt.
type MutationTransaction ¶
type MutationTransaction struct{}
MutationTransaction keeps the shared keyring lock across a multi-step credential commit, including its readback and rollback. Its methods skip re-locking; callers must use it only inside WithMutationTransaction.
func (*MutationTransaction) DeleteAPIKeyChecked ¶
func (*MutationTransaction) DeleteAPIKeyChecked() error
func (*MutationTransaction) DeleteClientSecretChecked ¶
func (*MutationTransaction) DeleteClientSecretChecked() error
func (*MutationTransaction) RestoreRemovedCredentialFiles ¶
func (*MutationTransaction) RestoreRemovedCredentialFiles(snapshot CredentialFileSnapshot) error
RestoreRemovedCredentialFiles writes back each snapshotted file that is now missing. It never overwrites or deletes a file: after a rollback, an existing file is the one the value restore wrote. Windows never writes credential files, so it restores nothing there.
func (*MutationTransaction) SaveClientSecretWithBackend ¶
func (*MutationTransaction) SaveClientSecretWithBackend(secret string) (CredentialBackend, error)
func (*MutationTransaction) SetAPIKeyWithBackend ¶
func (*MutationTransaction) SetAPIKeyWithBackend(key string) (CredentialBackend, error)
func (*MutationTransaction) SnapshotCredentialFiles ¶
func (*MutationTransaction) SnapshotCredentialFiles() CredentialFileSnapshot
SnapshotCredentialFiles records the API key and client secret files. A keyring write removes the file copy, and a login snapshot records only the effective keyring-first value, so a file that disagreed with the keyring would otherwise be lost when the login rolls back.
type ProbeOutcome ¶
type ProbeOutcome struct {
Slot string
Present bool
Result string
Recorded bool
// Cause is the sanitized probe failure; nil when Result is ok.
Cause error
}
ProbeOutcome is the value-free result of one remote credential probe.
func ProbeStoredCredential ¶
ProbeStoredCredential verifies one stored slot against the Tailscale API and records the outcome in credential-meta.json. The returned error covers only local failures (unreadable store, unknown slot); remote rejection is data in ProbeOutcome so doctor can classify it without parsing text.
type SlotMetadata ¶
type SlotMetadata struct {
Kind string `json:"kind"`
Fingerprint string `json:"fingerprint"`
StoredAt time.Time `json:"stored_at"`
ExpiresAt *time.Time `json:"expires_at"`
ExpiresAtSource string `json:"expires_at_source,omitempty"`
LastVerifiedAt *time.Time `json:"last_verified_at"`
LastVerifiedResult string `json:"last_verified_result,omitempty"`
}
SlotMetadata is the per-slot bookkeeping record. It never carries the credential value.
func NewSlotMetadata ¶
func NewSlotMetadata(slot, value string, opts StoredOptions) (SlotMetadata, error)
NewSlotMetadata builds the bookkeeping record for a credential that was just stored. The api-key slot defaults to the assumed 90-day maximum when the operator gives no expiry; the client-secret slot never expires.
func ReadSlotMetadata ¶
func ReadSlotMetadata(slot string) (*SlotMetadata, error)
ReadSlotMetadata returns the record for one slot, or nil when absent.
type SlotValues ¶
SlotValues carries the currently stored values only long enough to compute presence and fingerprints. Callers must not persist or print it.
func StoredSlotValues ¶
func StoredSlotValues() (SlotValues, error)
StoredSlotValues reads both credential slots for inventory purposes. Read failures are returned so callers can report them without guessing presence.
type SlotView ¶
type SlotView struct {
Slot string
Present bool
Metadata *SlotMetadata
Backfilled bool
ExpiryState string
DaysLeft *int
}
SlotView is the value-free report for one slot.
type StoredCredentialStatus ¶
type StoredCredentialStatus struct {
APIKey CredentialKindStatus
ClientSecret CredentialKindStatus
}
StoredCredentialStatus is the strict credential inventory used by destructive cleanup paths. It distinguishes not-found from unreadable/disabled stores.
func InspectStoredCredentialsStrict ¶
func InspectStoredCredentialsStrict() (StoredCredentialStatus, error)
InspectStoredCredentialsStrict reports whether API-key/client-secret credentials are present in every supported store. It fails closed when the keyring is disabled or unreadable because an old credential may remain there.
func (StoredCredentialStatus) AnyPresent ¶
func (s StoredCredentialStatus) AnyPresent() bool
type StoredOptions ¶
type StoredOptions struct {
Now time.Time
ExpiresAt *time.Time
ExpiresAtSource string
// Verified records that the login path proved the credential against the
// remote API before committing it.
Verified bool
}
StoredOptions describes what login learned about a freshly committed credential.
type TailscaleClientFactory ¶
TailscaleClientFactory constructs the REST client used to derive an auth key. Command callers can supply the tailapi loopback-override gate without creating an import cycle back from credentials to tailapi.