Skip to content

Commit 3ac1cbd

Browse files
authored
CUBE-59 - Add in enclave agent (#61)
* feat: Implement agent service with authentication and proxy capabilities Signed-off-by: Sammy Oina <sammyoina@gmail.com> * refactor: Remove authentication and identify endpoints, implement proxy service with enhanced TLS configuration Signed-off-by: Sammy Oina <sammyoina@gmail.com> * feat: Refactor agent and proxy services for improved TLS configuration and error handling - Added SPDX license headers to source files. - Changed agent service configuration to use pointers for better memory management. - Introduced a new function `setTLSConfig` to centralize TLS configuration logic. - Updated the `New` function in agent and proxy services to accept pointer types for configuration. - Enhanced error handling in the agent service's `New` function for TLS configuration. - Simplified the `Proxy` method in both agent and proxy services by removing unnecessary variables. - Refactored the `MakeHandler` function in both agent and proxy APIs to remove logger dependency. - Updated middleware implementations to streamline proxy handling. - Added new environment variables for Azure attestation configuration in the agent's main function. - Updated dependencies in `go.mod` and `go.sum` for improved functionality and security. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * refactor: Consolidate Dockerfile and Makefile for service compilation and building Signed-off-by: Sammy Oina <sammyoina@gmail.com> * fix: Add error handling for invalid URLs in agent and proxy services Signed-off-by: Sammy Oina <sammyoina@gmail.com> * refactor: Organize environment variables in proxy-compose.yaml for clarity Signed-off-by: Sammy Oina <sammyoina@gmail.com> * refactor: Update proxy service configuration and middleware for improved routing Signed-off-by: Sammy Oina <sammyoina@gmail.com> * refactor: Change package name from 'agent' to 'main' in main.go and remove unused linter from golangci.yaml Signed-off-by: Sammy Oina <sammyoina@gmail.com> --------- Signed-off-by: Sammy Oina <sammyoina@gmail.com>
1 parent cd1b544 commit 3ac1cbd

21 files changed

Lines changed: 772 additions & 272 deletions

File tree

‎.github/workflows/proxy-ci.yaml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ jobs:
3232
- name: Setup Go
3333
uses: actions/setup-go@v5
3434
with:
35-
go-version: 1.24.x
35+
go-version: 1.25.x
3636
cache-dependency-path: "go.sum"
3737

3838
- name: golangci-lint
39-
uses: golangci/golangci-lint-action@v7
39+
uses: golangci/golangci-lint-action@v8
4040
with:
41-
version: v2.0.2
41+
version: v2.4.0
4242
args: --config ./.golangci.yaml
4343

4444
- name: Build proxy

‎.golangci.yaml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ linters:
1313
- varnamelen
1414
- wrapcheck
1515
- wsl
16+
- godox
17+
- gosec
18+
- noinlineerr
1619
settings:
1720
gocritic:
1821
enabled-tags:

‎Makefile‎

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Copyright (c) Ultraviolet
22
# SPDX-License-Identifier: Apache-2.0
33

4-
CUBE_DOCKER_IMAGE_NAME ?= ghcr.io/ultravioletrs/cube/proxy
4+
CUBE_PROXY_DOCKER_IMAGE_NAME ?= ghcr.io/ultravioletrs/cube/proxy
5+
CUBE_AGENT_DOCKER_IMAGE_NAME ?= ghcr.io/ultravioletrs/cube/agent
56
CGO_ENABLED ?= 0
67
GOOS ?= linux
78
GOARCH ?= amd64
@@ -16,53 +17,87 @@ define compile_service
1617
-X 'github.com/absmach/supermq.BuildTime=$(TIME)' \
1718
-X 'github.com/absmach/supermq.Version=$(VERSION)' \
1819
-X 'github.com/absmach/supermq.Commit=$(COMMIT)'" \
19-
-o ${BUILD_DIR}/cube-proxy cmd/main.go
20+
-o ${BUILD_DIR}/cube-$(1) cmd/$(1)/main.go
2021
endef
2122

2223
define make_docker
2324
docker build \
2425
--no-cache \
26+
--build-arg SVC=$(1) \
2527
--build-arg GOOS=$(GOOS) \
2628
--build-arg GOARCH=$(GOARCH) \
2729
--build-arg VERSION=$(VERSION) \
2830
--build-arg COMMIT=$(COMMIT) \
29-
--tag=$(CUBE_DOCKER_IMAGE_NAME):$(VERSION) \
30-
--tag=$(CUBE_DOCKER_IMAGE_NAME):latest \
31+
--tag=$(2):$(VERSION) \
32+
--tag=$(2):latest \
3133
-f docker/Dockerfile .
3234
endef
3335

3436
define make_docker_dev
3537
docker build \
3638
--no-cache \
37-
--tag=$(CUBE_DOCKER_IMAGE_NAME):$(VERSION) \
38-
--tag=$(CUBE_DOCKER_IMAGE_NAME):latest \
39+
--build-arg SVC=$(1) \
40+
--tag=$(2):$(VERSION) \
41+
--tag=$(2):latest \
3942
-f docker/Dockerfile.dev ./build
4043
endef
4144

4245
define docker_push
43-
docker push $(CUBE_DOCKER_IMAGE_NAME):$(VERSION)
44-
docker push $(CUBE_DOCKER_IMAGE_NAME):latest
46+
docker push $(1):$(VERSION)
47+
docker push $(1):latest
4548
endef
4649

4750
.PHONY: build
48-
build:
49-
$(call compile_service)
51+
build: build-proxy build-agent
52+
53+
.PHONY: build-proxy
54+
build-proxy:
55+
$(call compile_service,proxy)
56+
57+
.PHONY: build-agent
58+
build-agent:
59+
$(call compile_service,agent)
5060

5161
.PHONY: docker
52-
docker:
53-
$(call make_docker)
62+
docker: docker-proxy docker-agent
63+
64+
.PHONY: docker-proxy
65+
docker-proxy:
66+
$(call make_docker,proxy,$(CUBE_PROXY_DOCKER_IMAGE_NAME))
67+
68+
.PHONY: docker-agent
69+
docker-agent:
70+
$(call make_docker,agent,$(CUBE_AGENT_DOCKER_IMAGE_NAME))
5471

5572
.PHONY: docker-dev
56-
docker-dev:
57-
$(call make_docker_dev)
73+
docker-dev: docker-proxy-dev docker-agent-dev
74+
75+
.PHONY: docker-proxy-dev
76+
docker-proxy-dev:
77+
$(call make_docker_dev,proxy,$(CUBE_PROXY_DOCKER_IMAGE_NAME))
78+
79+
.PHONY: docker-agent-dev
80+
docker-agent-dev:
81+
$(call make_docker_dev,agent,$(CUBE_AGENT_DOCKER_IMAGE_NAME))
5882

5983
all: build docker-dev
6084

6185
clean:
6286
rm -rf build
6387

6488
lint:
65-
golangci-lint run --config .golangci.yaml
89+
golangci-lint run --config .golangci.yaml
90+
91+
.PHONY: latest
92+
latest: docker docker-push
93+
94+
.PHONY: docker-push
95+
docker-push: docker-push-proxy docker-push-agent
96+
97+
.PHONY: docker-push-proxy
98+
docker-push-proxy:
99+
$(call docker_push,$(CUBE_PROXY_DOCKER_IMAGE_NAME))
66100

67-
latest: docker
68-
$(call docker_push)
101+
.PHONY: docker-push-agent
102+
docker-push-agent:
103+
$(call docker_push,$(CUBE_AGENT_DOCKER_IMAGE_NAME))

‎agent/agent.go‎

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
// Copyright (c) Ultraviolet
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package agent
5+
6+
import (
7+
"crypto/tls"
8+
"fmt"
9+
"log"
10+
"net/http"
11+
"net/http/httputil"
12+
"net/url"
13+
"time"
14+
15+
"github.com/absmach/supermq/api/http/util"
16+
"github.com/absmach/supermq/pkg/authn"
17+
"github.com/absmach/supermq/pkg/errors"
18+
"github.com/ultravioletrs/cocos/pkg/attestation"
19+
"github.com/ultravioletrs/cocos/pkg/attestation/quoteprovider"
20+
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
21+
)
22+
23+
var (
24+
// ErrAttestationFailed indicates that attestation failed.
25+
ErrAttestationFailed = errors.New("attestation failed")
26+
// ErrAttestationVTpmFailed indicates that vTPM attestation failed.
27+
ErrAttestationVTpmFailed = errors.New("vTPM attestation failed")
28+
// ErrAttestationType indicates that the attestation type is invalid.
29+
ErrAttestationType = errors.New("invalid attestation type")
30+
// ErrUnauthorized indicates that authentication failed.
31+
ErrUnauthorized = errors.New("unauthorized")
32+
)
33+
34+
type TLSConfig struct {
35+
Enabled bool
36+
InsecureSkipVerify bool
37+
CertFile string
38+
KeyFile string
39+
CAFile string
40+
MinVersion uint16
41+
MaxVersion uint16
42+
}
43+
44+
type Config struct {
45+
OllamaURL string
46+
TLS TLSConfig
47+
}
48+
49+
type agentService struct {
50+
config *Config
51+
provider attestation.Provider
52+
transport *http.Transport
53+
auth authn.Authentication
54+
}
55+
56+
type Service interface {
57+
Proxy() *httputil.ReverseProxy
58+
Attestation(
59+
reportData [quoteprovider.Nonce]byte, nonce [vtpm.Nonce]byte, attType attestation.PlatformType,
60+
) ([]byte, error)
61+
Authenticate(req *http.Request) error
62+
AuthMiddleware(next http.Handler) http.Handler
63+
}
64+
65+
func New(config *Config, auth authn.Authentication, provider attestation.Provider) (Service, error) {
66+
if config.OllamaURL == "" {
67+
return nil, errors.New("ollama URL is required")
68+
}
69+
70+
transport := &http.Transport{
71+
MaxIdleConns: 100,
72+
IdleConnTimeout: 90 * time.Second,
73+
TLSHandshakeTimeout: 10 * time.Second,
74+
}
75+
76+
if config.TLS.Enabled {
77+
tlsConfig, err := setTLSConfig(config)
78+
if err != nil {
79+
return nil, fmt.Errorf("failed to set TLS config: %w", err)
80+
}
81+
82+
transport.TLSClientConfig = tlsConfig
83+
}
84+
85+
return &agentService{
86+
config: config,
87+
transport: transport,
88+
provider: provider,
89+
auth: auth,
90+
}, nil
91+
}
92+
93+
func (a *agentService) Authenticate(req *http.Request) error {
94+
token := util.ExtractBearerToken(req)
95+
96+
if token == "" {
97+
return errors.Wrap(ErrUnauthorized, errors.New("missing or invalid token"))
98+
}
99+
100+
_, err := a.auth.Authenticate(req.Context(), token)
101+
if err != nil {
102+
return errors.Wrap(ErrUnauthorized, err)
103+
}
104+
105+
return nil
106+
}
107+
108+
func (a *agentService) Proxy() *httputil.ReverseProxy {
109+
target, err := url.Parse(a.config.OllamaURL)
110+
if err != nil {
111+
log.Printf("Invalid Ollama URL: %v", err)
112+
113+
return nil
114+
}
115+
116+
proxy := httputil.NewSingleHostReverseProxy(target)
117+
118+
proxy.Transport = a.transport
119+
120+
originalDirector := proxy.Director
121+
proxy.Director = func(req *http.Request) {
122+
originalDirector(req)
123+
a.modifyHeaders(req)
124+
log.Printf("Agent forwarding to Ollama: %s %s", req.Method, req.URL.Path)
125+
}
126+
127+
proxy.ErrorHandler = func(w http.ResponseWriter, _ *http.Request, err error) {
128+
log.Printf("Proxy error: %v", err)
129+
http.Error(w, "Bad Gateway", http.StatusBadGateway)
130+
}
131+
132+
return proxy
133+
}
134+
135+
func (a *agentService) Attestation(
136+
reportData [quoteprovider.Nonce]byte, nonce [vtpm.Nonce]byte, attType attestation.PlatformType,
137+
) ([]byte, error) {
138+
switch attType {
139+
case attestation.SNP, attestation.TDX:
140+
rawQuote, err := a.provider.TeeAttestation(reportData[:])
141+
if err != nil {
142+
return []byte{}, errors.Wrap(ErrAttestationFailed, err)
143+
}
144+
145+
return rawQuote, nil
146+
case attestation.VTPM:
147+
vTPMQuote, err := a.provider.VTpmAttestation(nonce[:])
148+
if err != nil {
149+
return []byte{}, errors.Wrap(ErrAttestationVTpmFailed, err)
150+
}
151+
152+
return vTPMQuote, nil
153+
case attestation.SNPvTPM:
154+
vTPMQuote, err := a.provider.Attestation(reportData[:], nonce[:])
155+
if err != nil {
156+
return []byte{}, errors.Wrap(ErrAttestationVTpmFailed, err)
157+
}
158+
159+
return vTPMQuote, nil
160+
case attestation.Azure, attestation.NoCC, attestation.AzureToken:
161+
return []byte{}, ErrAttestationType
162+
default:
163+
return []byte{}, ErrAttestationType
164+
}
165+
}
166+
167+
func (a *agentService) AuthMiddleware(next http.Handler) http.Handler {
168+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
169+
err := a.Authenticate(r)
170+
if err != nil {
171+
log.Printf("Authentication failed: %v", err)
172+
http.Error(w, "Unauthorized", http.StatusUnauthorized)
173+
174+
return
175+
}
176+
177+
next.ServeHTTP(w, r)
178+
})
179+
}
180+
181+
func DefaultTLSConfig() TLSConfig {
182+
return TLSConfig{
183+
Enabled: true,
184+
InsecureSkipVerify: false,
185+
MinVersion: tls.VersionTLS12,
186+
MaxVersion: tls.VersionTLS13,
187+
}
188+
}
189+
190+
func InsecureTLSConfig() TLSConfig {
191+
return TLSConfig{
192+
Enabled: true,
193+
InsecureSkipVerify: true,
194+
MinVersion: tls.VersionTLS12,
195+
MaxVersion: tls.VersionTLS13,
196+
}
197+
}
198+
199+
func (a *agentService) modifyHeaders(req *http.Request) {
200+
req.Header.Set("Content-Type", "application/json")
201+
202+
req.Header.Del("Authorization")
203+
}
204+
205+
func setTLSConfig(config *Config) (*tls.Config, error) {
206+
tlsConfig := &tls.Config{
207+
InsecureSkipVerify: config.TLS.InsecureSkipVerify,
208+
}
209+
210+
if config.TLS.MinVersion != 0 {
211+
tlsConfig.MinVersion = config.TLS.MinVersion
212+
}
213+
214+
if config.TLS.MaxVersion != 0 {
215+
tlsConfig.MaxVersion = config.TLS.MaxVersion
216+
}
217+
218+
if config.TLS.CertFile != "" && config.TLS.KeyFile != "" {
219+
cert, err := tls.LoadX509KeyPair(config.TLS.CertFile, config.TLS.KeyFile)
220+
if err != nil {
221+
return nil, fmt.Errorf("failed to load client certificate: %w", err)
222+
}
223+
224+
tlsConfig.Certificates = []tls.Certificate{cert}
225+
}
226+
227+
return tlsConfig, nil
228+
}

‎agent/api/transport.go‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) Ultraviolet
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package api
5+
6+
import (
7+
"net/http"
8+
9+
"github.com/absmach/supermq"
10+
"github.com/go-chi/chi/v5"
11+
"github.com/prometheus/client_golang/prometheus/promhttp"
12+
"github.com/ultraviolet/cube/agent"
13+
)
14+
15+
const ContentType = "application/json"
16+
17+
func MakeHandler(svc agent.Service, instanceID string) http.Handler {
18+
mux := chi.NewRouter()
19+
20+
mux.Handle("/", svc.AuthMiddleware(svc.Proxy()))
21+
22+
mux.Get("/health", supermq.Health("cube-agent", instanceID))
23+
mux.Handle("/metrics", promhttp.Handler())
24+
25+
return mux
26+
}

0 commit comments

Comments
 (0)