akas 2.4.18

AKAS: API Key Authorization Server
# AKAS: API Key Authorization Server

[![Software License](https://img.shields.io/badge/license-MIT-informational.svg?style=for-the-badge)](LICENSE)
[![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release&style=for-the-badge)](https://github.com/semantic-release/semantic-release)
[![crates.io](https://img.shields.io/crates/v/akas?logo=rust&color=fc8d62&style=for-the-badge)](https://crates.io/crates/akas)
[![Pipeline Status](https://img.shields.io/gitlab/pipeline-status/op_so/projects/akas?style=for-the-badge)](https://gitlab.com/op_so/projects/akas/pipelines)

A simple and higth performance server to authorized HTTP requests by API key checks.

- A hight performance server written in [Rust]https://www.rust-lang.org/,
- In-memory keys storage,
- Control authorization bearer with pre-checks,
- Perform a hot reload of the key file.

```txt
Authorization: Bearer <key>
```

![akas diagram](https://gitlab.com/op_so/projects/akas/-/raw/main/akas_diag.svg?ref_type=heads "AKAS diagram")

The file of the list of the keys to be used for authorization should contain one key per line in plain or [SHA-256](https://en.wikipedia.org/wiki/SHA-2) format:

- **sha256** (default)

```txt
8b89600015b273c28f966f368456e45e01df239a36bf939ff72a16881f775679
fb22be500af1ef0479745bbbce847854da33f5e910361ad278e0282995b95f4d
...
```

- **plain**

```txt
mykey-3532dceb-f38a-491b-814d-9607bc9a947a
mykey-c2d79a40-388e-4709-9e4b-903035b0e71e
...
```

![akas internal](https://gitlab.com/op_so/projects/akas/-/raw/main/akas_internal.svg?ref_type=heads "AKAS internal")

## Usage

```bash
$ ./akas --help
A HTTP API key-Based Authorization Server

Usage: akas <COMMAND>

Commands:
  serve  Start the server
  load   Load keys from file (Not yet implemented)
  help   Print this message or the help of the given subcommand(s)

Options:
  -h, --help     Print help
  -V, --version  Print version

$./akas serve --help
Start the server

Usage: akas serve [OPTIONS]

Options:
      --admin-key <ADMIN_KEY>
          Admin key for /load and /status URI [env: AKAS_ADMIN_KEY=] [default: ]
      --no-admin-key
          No admin key flag
      --local
          Bind local adress only [env: AKAS_LOCAL=]
      --enable-metrics
          Enable Prometheus metrics endpoint [env: AKAS_ENABLE_METRICS=]
  -p, --port <PORT>
          Port of the server [env: AKAS_PORT=] [default: 5001]
      --log-level <LOG_LEVEL>
          Log level <error|warn|info|debug|trace> [env: AKAS_LOG_LEVEL=] [default: info]
      --original-length <ORIGINAL_LENGTH>
          Length of the x-forwarded-for, x-original header fields [env: AKAS_ORIGINAL_LENGTH=] [default: 100]
      --metadata-length <METADATA_LENGTH>
          Length of the metadata header field [env: AKAS_METADATA_LENGTH=] [default: 0]
      --key-length <KEY_LENGTH>
          Length of the key [optional] [env: AKAS_KEY_LENGTH=] [default: 0]
      --key-prefix <KEY_PREFIX>
          Prefix of the key [optional] [env: AKAS_KEY_PREFIX=] [default: ]
  -h, --help
          Print help
```

### Start akas server with the default port `5001`

```bash
./akas serve --admin-key my-admin-key
```

### Example of configuration of a Nginx server

```text
server {
    listen     80;
    server_name _;

    location / {
      auth_request     /auth;
      auth_request_set $auth_status $upstream_status;
      root             /usr/share/nginx/html;
      index            index.html index.htm;
    }

    location = /auth {
      internal;
      proxy_pass              http://localhost:5001/auth;
      proxy_pass_request_body off;
      proxy_set_header        content-length         "";
      proxy_set_header        x-forwarded-for        $proxy_add_x_forwarded_for;
      proxy_set_header        x-original-host        $host;
      proxy_set_header        x-original-uri         $request_uri;
      proxy_set_header        x-akas-metadata        "my-metadata";
    }
}
```

More details of Nginx configuration can be found in the [_configuring subrequest authentication_ documentation](https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-subrequest-authentication/)

- Authorized request: `curl -H "Authorization: Bearer <key>" http://<host>/`

## Endpoints URIs

### `/auth`: Authorization endpoint

If the API key is present in the hashset
return `200 OK`,
otherwise return `401 Unauthorized`.

### `/load`

Load new plain/hash keys file and replace the current keys in HashSet. The access is protected by an optional admin key.
Example of a `curl` request:

```bash
curl -v \
  -H "Authorization: Bearer my-admin-key" \
  --url http://localhost:5001/load \
  -F 'json={"format": "sha256", "hash_input_file": "43fcba0b3a ..."};type=application/json' \
  -F file=@./tests/files/sha256_key.txt
```

| Field             | Description                          | Required | Default |
| ----------------- | ------------------------------------ | -------- | ------- |
| `file`            | File path of the keys file to upload | Yes      | -       |
| `format`          | Format of the keys <sha256\|plain>   | No       | sha256  |
| `hash_input_file` | sha-256 of the uploaded file         | No       | -       |

If the server is started without an admin key (`--no-admin-key`), the header `Authorization: Bearer` is still required with a fake key.

### `/status`

Return the application state in JSON format. The access is protected by an optional admin key.
Example:

```json
{
  "log_level": "INFO",
  "original_length": 100,
  "metadata_length": 0,
  "file_hash": "43fcba0b3a5ab1b302f9d13617ae4eec6ae623a7fd52437dd992d5dca115e68d",
  "file_date": "2024-11-09T14:07:23.416999558+00:00",
  "file_key_count": 150,
  "key_length": 42,
  "key_prefix": "mykey-"
}
```

Example of request:

```bash
curl -v  \
  -H "Authorization: Bearer my-admin-key" \
  http://localhost:5001/status
```

If the server is started without an admin key (`--no-admin-key`), the header `Authorization: Bearer` is still required with a fake key.

### `/metrics`

Enabling the `--enable-metrics` flag exposes Prometheus metrics via the `/metrics` endpoint:

``` text
# HELP akas_http_requests_duration_seconds HTTP request duration in seconds for all requests
# TYPE akas_http_requests_duration_seconds histogram
akas_http_requests_duration_seconds_bucket{endpoint="/auth",method="GET",status="401",le="0.005"} 1
akas_http_requests_duration_seconds_bucket{endpoint="/auth",method="GET",status="401",le="0.01"} 2
akas_http_requests_duration_seconds_bucket{endpoint="/auth",method="GET",status="401",le="0.025"} 2
akas_http_requests_duration_seconds_bucket{endpoint="/auth",method="GET",status="401",le="0.05"} 2
akas_http_requests_duration_seconds_bucket{endpoint="/auth",method="GET",status="401",le="0.1"} 2
akas_http_requests_duration_seconds_bucket{endpoint="/auth",method="GET",status="401",le="0.25"} 2
akas_http_requests_duration_seconds_bucket{endpoint="/auth",method="GET",status="401",le="0.5"} 2
akas_http_requests_duration_seconds_bucket{endpoint="/auth",method="GET",status="401",le="1"} 2
akas_http_requests_duration_seconds_bucket{endpoint="/auth",method="GET",status="401",le="2.5"} 2
akas_http_requests_duration_seconds_bucket{endpoint="/auth",method="GET",status="401",le="5"} 2
akas_http_requests_duration_seconds_bucket{endpoint="/auth",method="GET",status="401",le="10"} 2
akas_http_requests_duration_seconds_bucket{endpoint="/auth",method="GET",status="401",le="+Inf"} 2
akas_http_requests_duration_seconds_sum{endpoint="/auth",method="GET",status="401"} 0.005529125
akas_http_requests_duration_seconds_count{endpoint="/auth",method="GET",status="401"} 2
# HELP akas_http_requests_total Total number of HTTP requests
# TYPE akas_http_requests_total counter
akas_http_requests_total{endpoint="/auth",method="GET",status="401"} 2
# HELP auth_akas_requests_total Total number of requests for auth with custom labels
# TYPE auth_akas_requests_total counter
auth_akas_requests_total{endpoint="/auth",metadata="-",method="GET",status="401",x_original_host="-"} 2
```

### `/health`

Returns `200 OK` to indicate the service is healthy and operational.

### `/auth-unauthorized`

Always return `401 Unauthorized` without checking the key (for testing purposes or disable access).

## Features & Limitations

- [x] Authorization by HTTP Bearer key.
- [x] Configuration:
  - [x] via command line arguments.
  - [x] via environment variables.
- [x] Subcommand `serve`: Start server
- [ ] Subcommand `load`: Load file
- [ ] Subcommand `status`: Get Hash of input file, datetime of load, number of valid keys.
- [x] load plain keys file (plain text) by curl.
- [x] load hash keys file (hashed - sha256) by curl.
- [x] Plain or hashed keys loaded and saved in a Rust [HashSet]https://doc.rust-lang.org/std/collections/struct.HashSet.html for a fast authorization check.
- [x] Check of the key format during the loading process of the file based keys storage:
  - [x] prefix and length for plain keys file.
  - [x] SHA-256 for hashed keys file.
- [x] Initial check of the input key format in the header (length and prefix) [optional].
- [x] Endpoints:
  - [x] `/auth`: default endpoint.
  - [x] `/load`: Load new plain/hash keys file.
  - [x] `/status`: Get Hash of input file, datetime of load, number of valid keys.
  - [x] `/health`: indicate the service is healthy and operational.
  - [x] `/auth-unauthorized`: always return `401 Unauthorized` without checking the key.
- [x] Admin key in header bearer for `/load` and `/status`.
- [x] Binaries compatibility for Linux with no dependencies:
  - [x] x86-64 and arm64.
  - [x] glibc (debian, ubuntu, fedora...) and musl libc (alpine ...).
- [x] Tests:
  - [x] Unit tests (Rust).
  - [x] Functional tests (Robot Framework).
- [x] Gitlab CI/CD Pipeline to auto-publish new versions.
- [x] Renovate Bot auto-update dependencies.
- [ ] AKAS packaged in a distroless Docker image.
- [x] Log implemenations with `https://crates.io/crates/tracing-actix-web`
- [x] Log requests:
  - [x] All requests: Level Info
  - [x] Only unauthorized requests (401): Level Warn
  - [x] Json log format
  - [x] Length truncation of header fields
  - [x] Metadata field
- [x] Prometheus metrics
- [ ] `/load-diff` Load diff file
- [ ] Cache implementation for faster access to key authorization without SHA-256 operation (LRU Cache).

## Installation

- Binary file installation on Linux via the [GitLab package registry of the project]https://gitlab.com/op_so/projects/akas/-/packages:

  - 2 architectures:
    - akas-x86_64-linux-<gnu|musl>.tar.gz : x86_64 (Intel, AMD).
    - akas-aarch64-linux-<gnu|musl>.tar.gz : arm64
  - 2 C standard library with no dependencies:
    - akas-<x86_64|aarch64>-linux-gnu.tar.gz : glibc for Debian, Ubuntu, Fedora...
    - akas-<x86_64|aarch64>-linux-musl.tar.gz : musl libc for Alpine

- With a Rust environment, running this command will globally install the akas binary:

```shell
cargo install akas
```

## Log

The level of log is set with the `RUST_LOG` environment variable:

- error - Requests are not logged.
- warn - Only unauthorized requests (401) are logged.
- info (default) - all requests are logged.
- debug
- trace
- off

## Access log in jsonl format

```json
{
    "timestamp": "2025-05-29T22:25:56.133350Z",
    "level": "INFO",
    "fields": {
        "message": "access authorized",
        "key_hash": "92c55f4d88b1",
        "forwarded_for": "203.0.113.195",
        "original_host": "my-host.com",
        "original_uri": "/some/path",
        "metadata": "my-metadata",
        "access": "authorized"
    },
    "target": "akas"
}
{
    "timestamp": "2025-05-29T22:25:56.135564Z",
    "level": "WARN",
    "fields": {
        "message": "access unauthorized",
        "key_hash": "24328022c7341148e4d84fab687dff6bd1f7c836a73a307ceb3357a3b9bb2d9d",
        "forwarded_for": "203.0.113.195",
        "original_host": "my-host.com",
        "original_uri": "/some/path",
        "metadata": "my-metadata",
        "access": "unauthorized"
    },
    "target": "akas"
}
```

| Value                  | Description                                                             | Max field length                                                                  |
| ---------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `timestamp`            | Date and time in ISO 8601 format                                        |                                                                                   |
| `level`                | Log level                                                               |                                                                                   |
| `fields.message`       | Message                                                                 |                                                                                   |
| `fields.key_hash`      | sha256 key of the user, limited to 12 characters when authorized        |                                                                                   |
| `fields.forwarded_for` | Extract of `x-forwarded-for` header field set by nginx, if not set: `-` | `--original-length` option or `AKAS_ORIGINAL_LENGTH` env. variable [default: 100] |
| `fields.original_host` | Extract of `x-original-host` header field set by nginx, if not set: `-` | `--original-length` option or `AKAS_ORIGINAL_LENGTH` env. variable [default: 100] |
| `fields.original_uri`  | Extract of `x-original-uri` header field set by nginx, if not set: `-`  | `--original-length` option or `AKAS_ORIGINAL_LENGTH` env. variable [default: 100] |
| `fields.metadata`      | Extract of `x-akas-metadata` header field set by nginx, if not set: `-` | `--metadata-length` option or `AKAS_METADATA_LENGTH` env. variable [default: 0]   |
| `fields.access`        | `authorized`, `unauthorized`                                            |                                                                                   |
| `target`               | Component                                                               |                                                                                   |

More details:

- [Env logger]https://docs.rs/env_logger/latest/env_logger/

## Tests

AKAS employs two types of tests to ensure its quality:

- **Unit tests** are written in Rust.
- **Functional tests** are managed via [Robot Framework]https://robotframework.org/ and reside in a dedicated repository: [AKAS Functional Tests]https://gitlab.com/op_so/projects/akas-tests.

## Development

- Clone the source repository: `git clone https://gitlab.com/op_so/projects/akas.git`

- To format and lint:

```shell
cargo fmt  # cargo fmt -- --check
cargo clippy  # Rust linter
```

- To test:

```shell
cargo test  # Unit and integration tests
cargo tarpaulin --ignore-tests  # Code coverage
cargo audit  # Security audit
```

- To run: `cargo run`

- To build:

```shell
cargo build            # Debug binary target/debug/akas
cargo build --release  # Release binary target/release/akas
```

## Authors

<!-- vale off -->

- **FX Soubirou** - _Initial work_ - [GitLab repositories]https://gitlab.com/op_so
<!-- vale on -->

## License

<!-- vale off -->

This program is free software: you can redistribute it and/or modify it under the terms of the MIT License (MIT).
See the [LICENSE](https://opensource.org/licenses/MIT) for details.

<!-- vale on -->