The OpenZiti SDK for GoLang allows developers to create their own custom OpenZiti network endpoint clients and management tools. OpenZiti is a modern, programmable network overlay with associated edge components, for application-embedded, zero trust network connectivity, written by developers for developers. The SDK harnesses that power via APIs that allow developers to imagine and develop solutions beyond what OpenZiti handles by default.
This SDK does the following:
- enable network endpoint clients allow a device to dial (access) or bind (host) OpenZiti Services
- provides authentication interfaces for x509 certificates, username/password, external IdPs (JWT) flows
- collects and submits security posture collection/submission for Posture Checks
- allows Golang applications to bind or dial services via
net.Listener
andnet.Dialer
interfaces - enables raw access to the Ziti Edge Management API for custom management tooling of all OpenZiti network identities, policies, and more
- enables raw access to the Ziti Edge Client API for custom client tooling
This repository has a number of different folders, however below are the most important ones for a new developer to be aware of.
ziti
- the main SDK package that will be included in your projectedge-apis
- provides low-level abstractions for authenticating and accessing the Ziti Edge Client and Management APIsexample
- various example applications that illustrate different uses of the SDK. Each example contains its own README.md.chat
- a bare-bones example of a client and server for a chat program over an OpenZiti Servicechat-p2p
- highlightsaddressable terminators
which allows clients to dial specific services hosts if there are multiple hostscurlz
- wrapping existing network tooling (curl) to work over OpenZitigrpc-example
- using GRPC over OpenZitihttp-client
- a HTTP client accessing a web server over HTTPjwtchat
- highlights using external JWTs ( from OIDC/oAuth/etc.) to authenticate with OpenZItireflect
- a low level network "echo" client and server examplesimple-server
- a bare-bones HTTP server side only exampleudp-offload
- an example demonstrating how to work with an OpenZiti client and a UDP serverzcat
- wrapping existing network tooling (netcat) to work over OpenZitizping
- wrapping existing network tooling (ping) to work over OpenZiti
An "endpoint client" in OpenZiti's language is an identity that is dialing (accessing) or binding (hosting) a service. Dialing contacts either another identity hosting a service, which may be another client endpoint, or it may be handled by an Edge Router depending on its termination configuration. This SDK supports binding and dialing, which means it can host or access services depending on what it is instructed to do and the policies affecting the software's identity and service(s).
To test a client endpoint you will need the following outside your normal Golang development environment:
- An OpenZiti Network with controller with at least one Edge Router (See Quick Starts)
- A service to dial (access) and bind (host) (See Allowing Dial/Bind Access To A Service)
- An identity for your client to test with (See Creating & Enrolling a Dial Identity)
The steps for writing any endpoint client are:
The above links provide the steps in more detail, but here is the most basic setup to dial a service with most error handling removed for brevity:
cfg, _ := ziti.NewConfigFromFile("client.json")
context, _ := ziti.NewContext(cfg)
conn, _ := context.Dial(serviceName)
if _, err := conn.Write([]byte("hello I am myTestClient")); err != nil {
panic(err)
}
Configuration can be done through a file or through code that creates a Config
instance. Loading
through a file support x509 authentication only while creating custom Config
instances allows for all authentication
methods (x509, Username/Password, JWT, etc.).
The easiest way to create a configuration is by using
the ziti edge enroll
capabilities that will
generate an identity file that provides the location of the OpenZiti controller, the configuration types the client is
interested in, and the x509 certificate and private key to use.
cfg, err := ziti.NewConfigFromFile("client.json")
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "failed to read configuration: %v", err)
os.Exit(1)
}
// Note that GetControllerWellKnownCaPool() does not verify the authenticity of the controller, it is assumed
// this is handled in some other way.
caPool, err := ziti.GetControllerWellKnownCaPool("https://localhost:1280")
if err != nil {
panic(err)
}
credentials := edge_apis.NewUpdbCredentials("Joe Admin", "20984hgn2q048ngq20-3gn")
credentials.CaPool = caPool
cfg := &ziti.Config{
ZtAPI: "https://localhost:1280/edge/client/v1",
Credentials: credentials,
}
ctx, err := ziti.NewContext(cfg)
A Context
instances represent a specific identity connected to a Ziti Controller. The instance,
once configured, will handle authentication, re-authentication, posture state submission, and provides interfaces
to dial/bind services.
context, err := ziti.NewContext(cfg)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "failed to create context: %v", err)
os.Exit(1)
}
The main activity performed with a Context
is to dial or bind a service. In order for a dial or
bind to be successful, the following must be true:
- The identity must have the proper dial or bind service policy to the service via Service Policies
- The identity must have the proper dial or bind services over at least one Edge Router via Edge Router Policies
- The service must be allowed to be dialed or bound on at least one Edge Router via Service Edge Router Policies)
The easiest way to satisfy #2 and #3 are the make use of the #all
role attribute
when creating the policies. Edge Router policies and Service Edge Router Policies are useful for geographic connection
management. For smaller networks, test networks, and networks without geographic network entry are not concerns they
add complexity without inherent benefit. Using the #all
role attributes makes all service accessible and valid
dial/bind targets on all Edge Routers.
> ziti edge create service-edge-router-policy serp-all --edge-router-roles "#all" --service-roles "#all"
> ziti edge create edge-router-policy erp-all --edge-router-roles "#all" --identity-roles "#all"
> ziti edge create service-policy testDial Dial --identity-roles "@myTestClient" --service-roles "@myChat"
> ziti edge create service-policy testBind Bind --identity-roles "@myTestServer" --service-roles "@myChat"
Note: While policies can be created targeting specific users, services, or routers, using #attribute
style assignments
allows you to grant access based on groupings. (See Roles and Role Attributes)
conn, err := context.Dial(serviceName)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "failed to dial service %v, err: %+v\n", serviceName, err)
os.Exit(1)
}
if _, err := conn.Write([]byte("hello I am myTestClient")); err != nil {
panic(err)
}
Note: A full implementation will have to accept connections, hand them off to another goroutine and then re-wait on
listener.Accept()
func main(){
//... load configuration, create context
listener, err := context.ListenWithOptions(serviceName, &options)
if err != nil {
logrus.Errorf("Error binding service %+v", err)
panic(err)
}
for {
conn, err := listener.Accept()
if err != nil {
logger.Errorf("server error, exiting: %+v\n", err)
panic(err)
}
logger.Infof("new connection")
go handleConn(conn)
}
}
func handleConn(conn net.Conn){
for {
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
_ = conn.Close()
return
}
stringData := string(buf[:n])
println(stringData)
}
}
For more detail on how to create and enroll identities see the identities section in the OpenZiti documentation.
- Login to the controller
ziti edge login https://ctrl-api/edge/client/v1 -u <username> -p <password>
- Create a new identity
ziti edge create identity device myTestClient -o client.enroll.jwt
- Enroll the identity
ziti edge enroll client.enroll.jwt -o client.json
The output file, client.json
in this file, is used as that target in the SDK call
ziti.NewConfigFromFile("client.json")
to create a configuration.
For more detail on policies see the policies section in the OpenZiti documentation.
- Login if not already logged in
ziti edge login https://ctrl-api/edge/client/v1 -u <username> -p <password>
- Create a new service
ziti edge create service myChat
- Allow the service to be accessed by the
myTestClient
through any Edge Router and the servicemyChat
through any Edge Routerziti edge create service-policy testPolicy Dial --identity-roles "@myTestClient" --service-roles "@myChat"
ziti edge create service-edge-router-policy chatOverAll --edge-router-roles "#all" --service-roles "@myChat"
Note: While policies can be created targeting specific users, services, or routers, using #attribute
style assignments
allows you to grant access based on groupings. (See Roles and Role Attributes)
The Edge Management and Client APIs are defined by an OpenAPI 2.0 specification and have a client that is generated
and maintained in another GitHub repository. Accessing this repository directly
should not be necessary. This SDK provides a wrapper around the generated clients found in edge-apis
.
apiUrl, _ = url.Parse("https://localhost:1280/edge/management/v1")
// Note that GetControllerWellKnownCaPool() does not verify the authenticity of the controller, it is assumed
// this is handled in some other way.
caPool, err := ziti.GetControllerWellKnownCaPool("https://localhost:1280")
if err != nil {
panic(err)
}
credentials := edge_apis.NewUpdbCredentials("Joe Admin", "20984hgn2q048ngq20-3gn")
credentials.CaPool = caPool
//Note: the CA pool can be provided here or during the Authenticate(<creds>) call. It is allowed here to enable
// calls to REST API endpoints that do not require authentication.
managementClient := edge_apis.NewManagementApiClient(apiUrl, credentials.GetCaPool()),
//"configTypes" are string identifiers of configuration that can be requested by clients. Developers may
//specify their own in order to provide distributed identity and/or service specific configurations.
//
//See: https://openziti.io/docs/learn/core-concepts/config-store/overview
//Example: configTypes = []string{"myCustomAppConfigType"}
var configTypes []string
apiSesionDetial, err := managementClient.Authenticate(credentials, configTypes)
apiUrl, _ = url.Parse("https://localhost:1280/edge/client/v1")
// Note that GetControllerWellKnownCaPool() does not verify the authenticity of the controller, it is assumed
// this is handled in some other way.
caPool, err := ziti.GetControllerWellKnownCaPool("https://localhost:1280")
if err != nil {
panic(err)
}
credentials := edge_apis.NewUpdbCredentials("Joe Admin", "20984hgn2q048ngq20-3gn")
credentials.CaPool = caPool
//Note: the CA pool can be provided here or during the Authenticate(<creds>) call. It is allowed here to enable
// calls to REST API endpoints that do not require authentication.
client := edge_apis.NewClientApiClient(apiUrl, credentials.GetCaPool()),
//"configTypes" are string identifiers of configuration that can be requested by clients. Developers may
//specify their own in order to provide distributed identity and/or service specific configurations. The
//OpenZiti tunnelers use this capability to configure interception of network connections.
//See: https://openziti.io/docs/learn/core-concepts/config-store/overview
//Example: configTypes = []string{"myCustomAppConfigType"}
var configTypes []string
apiSesionDetial, err := client.Authenticate(credentials, configTypes)
The following example show how to list services. Altering the names of the package types used will allow the same code to work for the Edge Client API.
// GetServices retrieves services in chunks of 500 till it has accumulated all services.
func GetServices(client *apis.ManagementApiClient) ([]*rest_model.ServiceDetail, error) {
params := service.NewListServicesParams()
pageOffset := int64(0)
pageLimit := int64(500)
var services []*rest_model.ServiceDetail
for {
params.Limit = &pageLimit
params.Offset = &pageOffset
resp, err := client.API.Service.ListServices(params, nil)
if err != nil {
return nil, rest_util.WrapErr(err)
}
if services == nil {
services = make([]*rest_model.ServiceDetail, 0, *resp.Payload.Meta.Pagination.TotalCount)
}
services = append(services, resp.Payload.Data...)
pageOffset += pageLimit
if pageOffset >= *resp.Payload.Meta.Pagination.TotalCount {
break
}
}
return services, nil
}